From 01bf2d72a9cfe24b31d2513a03fbe48e189eec8b Mon Sep 17 00:00:00 2001 From: edi-oai Date: Tue, 8 Sep 2026 18:40:52 +0100 Subject: [PATCH] Support port-forwarding directly to Tart Guest Agent (#489) --- .golangci.yml | 3 + api/openapi.yaml | 18 ++- go.mod | 39 +++--- go.sum | 82 ++++++------ internal/controller/api_rpc_watch.go | 6 + internal/controller/api_vms_exec.go | 1 + internal/controller/api_vms_portforward.go | 37 ++++-- .../controller/api_workers_portforward.go | 2 +- internal/tests/port_forward_test.go | 53 ++++++++ internal/worker/hostprocess/process.go | 21 +-- internal/worker/hostprocess/process_test.go | 58 -------- internal/worker/rpc.go | 22 ++-- internal/worker/rpcv2.go | 56 +++++++- internal/worker/socketalias/socketalias.go | 67 ++++++++++ .../worker/socketalias/socketalias_test.go | 49 +++++++ pkg/client/vms.go | 28 ++++ pkg/resource/v1/service_account_role.go | 18 ++- pkg/resource/v1/watch_instruction.go | 7 +- rpc/orchard.pb.go | 124 ++++++++++++++---- rpc/orchard.proto | 8 +- 20 files changed, 500 insertions(+), 199 deletions(-) create mode 100644 internal/tests/port_forward_test.go delete mode 100644 internal/worker/hostprocess/process_test.go create mode 100644 internal/worker/socketalias/socketalias.go create mode 100644 internal/worker/socketalias/socketalias_test.go diff --git a/.golangci.yml b/.golangci.yml index ab98339..1ebf01b 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -83,6 +83,9 @@ linters: # Avoid unrelated style churn for now - funcorder + # It's OK to have dynamic errors since we're not a library + - err113 + issues: # Don't hide multiple issues that belong to one class since GitHub annotations can handle them all nicely. max-issues-per-linter: 0 diff --git a/api/openapi.yaml b/api/openapi.yaml index 598842c..6155f68 100644 --- a/api/openapi.yaml +++ b/api/openapi.yaml @@ -380,12 +380,17 @@ paths: type: string get: summary: "Port-forward to a VM using WebSocket protocol" + description: | + Connect to a VM's TCP port, a declared host process, or its Tart Guest Agent. + Specify exactly one of `port`, `hostProcess`, or `target=tart-guest-agent`. + + Connecting to Tart Guest Agent requires either the `compute:write` or `compute:connect:tart-guest-agent` role. tags: - vms parameters: - in: query name: port - description: VM's TCP port number to connect to + description: VM's TCP port number to connect to; mutually exclusive with `hostProcess` and `target`. schema: type: integer minimum: 1 @@ -393,10 +398,17 @@ paths: required: false - in: query name: hostProcess - description: Name of a host process declared in the VM's `hostProcesses` field; mutually exclusive with `port`. + description: Name of a host process declared in the VM's `hostProcesses` field; mutually exclusive with `port` and `target`. schema: type: string required: false + - in: query + name: target + description: Forward to the specified target; mutually exclusive with `port` and `hostProcess`. + schema: + type: string + enum: [tart-guest-agent] + required: false - in: query name: wait description: Duration in seconds to wait for the VM to transition into "running" state if not already running. @@ -420,7 +432,7 @@ paths: type: string responses: '400': - description: Invalid or ambiguous port-forward target specified + description: Invalid query parameter or ambiguous port-forward target specified '404': description: VM or requested host process doesn't exist '503': diff --git a/go.mod b/go.mod index 03c81a6..c0851a9 100644 --- a/go.mod +++ b/go.mod @@ -9,6 +9,7 @@ require ( github.com/avast/retry-go/v4 v4.7.0 github.com/avast/retry-go/v5 v5.0.0 github.com/cirruslabs/chacha v0.16.3 + github.com/cirruslabs/tart-guest-agent v0.14.2 github.com/coder/websocket v1.8.14 github.com/deckarep/golang-set/v2 v2.8.0 github.com/dgraph-io/badger/v3 v3.2103.5 @@ -22,36 +23,36 @@ require ( github.com/google/uuid v1.6.0 github.com/gosuri/uitable v0.0.4 github.com/hashicorp/go-multierror v1.1.1 - github.com/hashicorp/go-version v1.8.0 + github.com/hashicorp/go-version v1.9.0 github.com/manifoldco/promptui v0.9.0 github.com/mitchellh/go-grpc-net-conn v0.0.0-20200427190222-eb030e4876f0 github.com/pkg/errors v0.9.1 github.com/pterm/pterm v0.12.83 - github.com/puzpuzpuz/xsync/v4 v4.4.0 + github.com/puzpuzpuz/xsync/v4 v4.5.0 github.com/samber/lo v1.53.0 github.com/samber/mo v1.16.0 github.com/sethvargo/go-password v0.3.1 github.com/shirou/gopsutil/v4 v4.26.2 github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 github.com/spf13/cobra v1.10.2 - github.com/stretchr/testify v1.11.1 + github.com/stretchr/testify v1.12.0 go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin v0.67.0 - go.opentelemetry.io/otel v1.42.0 + go.opentelemetry.io/otel v1.44.0 go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.42.0 go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.42.0 - go.opentelemetry.io/otel/metric v1.42.0 - go.opentelemetry.io/otel/sdk/metric v1.42.0 - go.uber.org/zap v1.27.1 - golang.org/x/crypto v0.49.0 + go.opentelemetry.io/otel/metric v1.44.0 + go.opentelemetry.io/otel/sdk/metric v1.44.0 + go.uber.org/zap v1.28.0 + golang.org/x/crypto v0.51.0 golang.org/x/exp v0.0.0-20250218142911-aa4b98e5adaa golang.org/x/exp/jsonrpc2 v0.0.0-20260718201538-764159d718ef - golang.org/x/net v0.52.0 - golang.org/x/sync v0.20.0 - golang.org/x/sys v0.46.0 - golang.org/x/term v0.41.0 - golang.org/x/text v0.35.0 - google.golang.org/grpc v1.79.3 - google.golang.org/protobuf v1.36.11 + golang.org/x/net v0.55.0 + golang.org/x/sync v0.22.0 + golang.org/x/sys v0.47.0 + golang.org/x/term v0.43.0 + golang.org/x/text v0.37.0 + google.golang.org/grpc v1.83.0 + google.golang.org/protobuf v1.36.12 gopkg.in/natefinch/lumberjack.v2 v2.2.1 gopkg.in/yaml.v3 v3.0.1 howett.net/plist v1.0.1 @@ -71,9 +72,8 @@ require ( github.com/clipperhouse/uax29/v2 v2.7.0 // indirect github.com/cloudwego/base64x v0.1.6 // indirect github.com/containerd/console v1.0.5 // indirect - github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/dgraph-io/ristretto v0.1.1 // indirect - github.com/ebitengine/purego v0.10.0 // indirect + github.com/ebitengine/purego v0.10.1 // indirect github.com/fatih/color v1.18.0 // indirect github.com/gabriel-vasile/mimetype v1.4.13 // indirect github.com/gin-contrib/sse v1.1.0 // indirect @@ -125,7 +125,6 @@ require ( github.com/modern-go/reflect2 v1.0.2 // indirect github.com/oklog/ulid/v2 v2.1.1 // indirect github.com/pelletier/go-toml/v2 v2.2.4 // indirect - github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect github.com/quic-go/qpack v0.6.0 // indirect github.com/quic-go/quic-go v0.59.0 // indirect @@ -139,8 +138,8 @@ require ( go.mongodb.org/mongo-driver/v2 v2.5.0 // indirect go.opencensus.io v0.22.5 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect - go.opentelemetry.io/otel/sdk v1.42.0 // indirect - go.opentelemetry.io/otel/trace v1.42.0 // indirect + go.opentelemetry.io/otel/sdk v1.44.0 // indirect + go.opentelemetry.io/otel/trace v1.44.0 // indirect go.opentelemetry.io/proto/otlp v1.9.0 // indirect go.uber.org/multierr v1.11.0 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect diff --git a/go.sum b/go.sum index 98e9609..6c22a70 100644 --- a/go.sum +++ b/go.sum @@ -47,6 +47,8 @@ github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 h1:q763qf9huN11kDQavWs github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/cirruslabs/chacha v0.16.3 h1:rOxn+qXsF3N9tQDpYxJ99e1ZP7G+IsFpzXd47IfSVIg= github.com/cirruslabs/chacha v0.16.3/go.mod h1:6rPd7APYL3lPLA6L9tP5KAwx3l0nYjFA+1o2amYoICI= +github.com/cirruslabs/tart-guest-agent v0.14.2 h1:m3pKQ7NYTVKb5whX00AhOXOUOfUXArqLApHWjIWjyL4= +github.com/cirruslabs/tart-guest-agent v0.14.2/go.mod h1:zB/fw/qgmS/Ah0idRH2PlYhbtwlQ1/7CeHHqzCigWlM= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk= github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM= @@ -78,8 +80,8 @@ github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUn github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= -github.com/ebitengine/purego v0.10.0 h1:QIw4xfpWT6GWTzaW5XEKy3HXoqrJGx1ijYHzTF0/ISU= -github.com/ebitengine/purego v0.10.0/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= +github.com/ebitengine/purego v0.10.1 h1:dewVBCBT2GaMu1SrNTYxQhgQBethzfhiwvZiLGP/qyY= +github.com/ebitengine/purego v0.10.1/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= @@ -202,8 +204,8 @@ github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/U github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= -github.com/hashicorp/go-version v1.8.0 h1:KAkNb1HAiZd1ukkxDFGmokVZe1Xy9HG6NUp+bPle2i4= -github.com/hashicorp/go-version v1.8.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= +github.com/hashicorp/go-version v1.9.0 h1:CeOIz6k+LoN3qX9Z0tyQrPtiB1DFYRPfCIBtaXPSCnA= +github.com/hashicorp/go-version v1.9.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= @@ -263,8 +265,6 @@ github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8 github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= -github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU= github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= @@ -277,8 +277,8 @@ github.com/pterm/pterm v0.12.36/go.mod h1:NjiL09hFhT/vWjQHSj1athJpx6H8cjpHXNAK5b github.com/pterm/pterm v0.12.40/go.mod h1:ffwPLwlbXxP+rxT0GsgDTzS3y3rmpAO1NMjUkGTYf8s= github.com/pterm/pterm v0.12.83 h1:ie+YmGmA727VuhxBlyGr74Ks+7McV6kT99IB8EU80aA= github.com/pterm/pterm v0.12.83/go.mod h1:xlgc6bFWyJIMtmLJvGim+L7jhSReilOlOnodeIYe4Tk= -github.com/puzpuzpuz/xsync/v4 v4.4.0 h1:vlSN6/CkEY0pY8KaB0yqo/pCLZvp9nhdbBdjipT4gWo= -github.com/puzpuzpuz/xsync/v4 v4.4.0/go.mod h1:VJDmTCJMBt8igNxnkQd86r+8KUeN1quSfNKu5bLYFQo= +github.com/puzpuzpuz/xsync/v4 v4.5.0 h1:vOSWu6b57/emh+L/Cw0BeQfvxa/cogFywXHeGUxQxAg= +github.com/puzpuzpuz/xsync/v4 v4.5.0/go.mod h1:VJDmTCJMBt8igNxnkQd86r+8KUeN1quSfNKu5bLYFQo= github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8= github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII= github.com/quic-go/quic-go v0.59.0 h1:OLJkp1Mlm/aS7dpKgTc6cnpynnD2Xg7C1pwL6vy/SAw= @@ -327,8 +327,8 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= -github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/stretchr/testify v1.12.0 h1:K6Mr6jO9JICuend/5xzTM03ydSV3vdNRYAdPSukj8uI= +github.com/stretchr/testify v1.12.0/go.mod h1:bOYBZb5qJ00vPzWfIqBUZPaxK8jWiXc6d3ErP4Ca9Gw= github.com/tklauser/go-sysconf v0.3.16 h1:frioLaCQSsF5Cy1jgRBrzr6t502KIIwQ0MArYICU0nA= github.com/tklauser/go-sysconf v0.3.16/go.mod h1:/qNL9xxDhc7tx3HSRsLWNnuzbVfh3e7gh/BmM179nYI= github.com/tklauser/numcpus v0.11.0 h1:nSTwhKH5e1dMNsCdVBukSZrURJRoHbSEQjdEbY+9RXw= @@ -357,22 +357,24 @@ go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin v0. go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin v0.67.0/go.mod h1:WB2cS9y+AwqqKhoo9gw6/ZxlSjFBUQGZ8BQOaD3FVXM= go.opentelemetry.io/contrib/propagators/b3 v1.42.0 h1:B2Pew5ufEtgkjLF+tSkXjgYZXQr9m7aCm1wLKB0URbU= go.opentelemetry.io/contrib/propagators/b3 v1.42.0/go.mod h1:iPgUcSEF5DORW6+yNbdw/YevUy+QqJ508ncjhrRSCjc= -go.opentelemetry.io/otel v1.42.0 h1:lSQGzTgVR3+sgJDAU/7/ZMjN9Z+vUip7leaqBKy4sho= -go.opentelemetry.io/otel v1.42.0/go.mod h1:lJNsdRMxCUIWuMlVJWzecSMuNjE7dOYyWlqOXWkdqCc= +go.opentelemetry.io/otel v1.44.0 h1:JjwHmHpA4iZ3wBxluu2fbbE7j4kqlE8jXyAyPXH7HqU= +go.opentelemetry.io/otel v1.44.0/go.mod h1:BMgjTHL9WPRlRjL2oZCBTL4whCGtXch2H4BhOPIAyYc= go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.42.0 h1:MdKucPl/HbzckWWEisiNqMPhRrAOQX8r4jTuGr636gk= go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.42.0/go.mod h1:RolT8tWtfHcjajEH5wFIZ4Dgh5jpPdFXYV9pTAk/qjc= go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.42.0 h1:H7O6RlGOMTizyl3R08Kn5pdM06bnH8oscSj7o11tmLA= go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.42.0/go.mod h1:mBFWu/WOVDkWWsR7Tx7h6EpQB8wsv7P0Yrh0Pb7othc= go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.42.0 h1:s/1iRkCKDfhlh1JF26knRneorus8aOwVIDhvYx9WoDw= go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.42.0/go.mod h1:UI3wi0FXg1Pofb8ZBiBLhtMzgoTm1TYkMvn71fAqDzs= -go.opentelemetry.io/otel/metric v1.42.0 h1:2jXG+3oZLNXEPfNmnpxKDeZsFI5o4J+nz6xUlaFdF/4= -go.opentelemetry.io/otel/metric v1.42.0/go.mod h1:RlUN/7vTU7Ao/diDkEpQpnz3/92J9ko05BIwxYa2SSI= -go.opentelemetry.io/otel/sdk v1.42.0 h1:LyC8+jqk6UJwdrI/8VydAq/hvkFKNHZVIWuslJXYsDo= -go.opentelemetry.io/otel/sdk v1.42.0/go.mod h1:rGHCAxd9DAph0joO4W6OPwxjNTYWghRWmkHuGbayMts= -go.opentelemetry.io/otel/sdk/metric v1.42.0 h1:D/1QR46Clz6ajyZ3G8SgNlTJKBdGp84q9RKCAZ3YGuA= -go.opentelemetry.io/otel/sdk/metric v1.42.0/go.mod h1:Ua6AAlDKdZ7tdvaQKfSmnFTdHx37+J4ba8MwVCYM5hc= -go.opentelemetry.io/otel/trace v1.42.0 h1:OUCgIPt+mzOnaUTpOQcBiM/PLQ/Op7oq6g4LenLmOYY= -go.opentelemetry.io/otel/trace v1.42.0/go.mod h1:f3K9S+IFqnumBkKhRJMeaZeNk9epyhnCmQh/EysQCdc= +go.opentelemetry.io/otel/metric v1.44.0 h1:1w0gILTcHdr3YI+ixLyjemwrVnsMURbTZFrSYCdDdmc= +go.opentelemetry.io/otel/metric v1.44.0/go.mod h1:8O7hanEPBNgEMmybD3s2VBKcgWOCsA6tzHBPODAiquo= +go.opentelemetry.io/otel/metric/x v0.66.0 h1:YkCrx1zLOChi9ZcZ6euupOcsgzbVlec7D/xoEU1+cTA= +go.opentelemetry.io/otel/metric/x v0.66.0/go.mod h1:d1+BDj9t96do0/1LoU1ayfCv79ZgNE41qbhBvnMOBZk= +go.opentelemetry.io/otel/sdk v1.44.0 h1:nHYwb9lK+fJPU/dnT6s7W7Z8itMWyqrnVfbheVYrZ58= +go.opentelemetry.io/otel/sdk v1.44.0/go.mod h1:Osuydd3Se74nqjAKxid74N5eC+jfEqfTegHRnq58oK0= +go.opentelemetry.io/otel/sdk/metric v1.44.0 h1:3LlKgI+VjbVsjNRFZJZAJ30WjXC5VkNRks6si09iEfI= +go.opentelemetry.io/otel/sdk/metric v1.44.0/go.mod h1:5B5pMARnXxKhltooO4xUuCBorl65a4EpnTalObqOigA= +go.opentelemetry.io/otel/trace v1.44.0 h1:jxF5CsGYCe74MCRx2X4g7WsY/VBKRqqpNvXlX/6gtIk= +go.opentelemetry.io/otel/trace v1.44.0/go.mod h1:oLl1jrMQAVo6v3GAggN+1VH9VIz9iUSvW53sW1Q8PIE= go.opentelemetry.io/proto/otlp v1.9.0 h1:l706jCMITVouPOqEnii2fIAuO3IVGBRPV5ICjceRb/A= go.opentelemetry.io/proto/otlp v1.9.0/go.mod h1:xE+Cx5E/eEHw+ISFkwPLwCZefwVjY+pqKg1qcK03+/4= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= @@ -381,8 +383,8 @@ go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y= go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= -go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc= -go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +go.uber.org/zap v1.28.0 h1:IZzaP1Fv73/T/pBMLk4VutPl36uNC+OSUh3JLG3FIjo= +go.uber.org/zap v1.28.0/go.mod h1:rDLpOi171uODNm/mxFcuYWxDsqWSAVkFdX4XojSKg/Q= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/arch v0.24.0 h1:qlJ3M9upxvFfwRM51tTg3Yl+8CP9vCC1E7vlFpgv99Y= @@ -392,8 +394,8 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4= -golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA= +golang.org/x/crypto v0.51.0 h1:IBPXwPfKxY7cWQZ38ZCIRPI50YLeevDLlLnyC5wRGTI= +golang.org/x/crypto v0.51.0/go.mod h1:8AdwkbraGNABw2kOX6YFPs3WM22XqI4EXEd8g+x7Oc8= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20250218142911-aa4b98e5adaa h1:t2QcU6V556bFjYgu4L6C+6VrCPyJZ+eyRsABUPs1mz4= golang.org/x/exp v0.0.0-20250218142911-aa4b98e5adaa/go.mod h1:BHOTPb3L19zxehTsLoJXVaTktb06DFgmdW6Wb9s8jqk= @@ -419,8 +421,8 @@ golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwY golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= -golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0= -golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw= +golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8= +golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -430,8 +432,8 @@ golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= -golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= +golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek= +golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -454,22 +456,22 @@ golang.org/x/sys v0.0.0-20221010170243-090e33056c14/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw= -golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs= +golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= -golang.org/x/term v0.41.0 h1:QCgPso/Q3RTJx2Th4bDLqML4W6iJiaXFq2/ftQF13YU= -golang.org/x/term v0.41.0/go.mod h1:3pfBgksrReYfZ5lvYM0kSO0LIkAl4Yl2bXOkKP7Ec2A= +golang.org/x/term v0.43.0 h1:S4RLU2sB31O/NCl+zFN9Aru9A/Cq2aqKpTZJ6B+DwT4= +golang.org/x/term v0.43.0/go.mod h1:lrhlHNdQJHO+1qVYiHfFKVuVioJIheAc3fBSMFYEIsk= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= -golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8= -golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA= +golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc= +golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= @@ -486,8 +488,8 @@ golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da h1:noIWHXmPHxILtqtCOPIhSt0ABwskkZKjD3bXGnZGpNY= golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90= -gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= -gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= +gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= +gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= @@ -500,10 +502,10 @@ google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiq google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= google.golang.org/grpc v1.28.1/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= -google.golang.org/grpc v1.79.3 h1:sybAEdRIEtvcD68Gx7dmnwjZKlyfuc61Dyo9pGXXkKE= -google.golang.org/grpc v1.79.3/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= -google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= -google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +google.golang.org/grpc v1.83.0 h1:JeNZEKJFbQxArAMl+hiytHauacDNqJUllNfmIMmpqnQ= +google.golang.org/grpc v1.83.0/go.mod h1:kDyl6SKsiHKt0uylY5gtn5cEjkrIOhQOGDgIc4JGwzQ= +google.golang.org/protobuf v1.36.12 h1:pJOKDDOyeXErUroCihFAd5LQuwXBSpVnKGrj5o/fwxc= +google.golang.org/protobuf v1.36.12/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= diff --git a/internal/controller/api_rpc_watch.go b/internal/controller/api_rpc_watch.go index 8fa3c99..73ca879 100644 --- a/internal/controller/api_rpc_watch.go +++ b/internal/controller/api_rpc_watch.go @@ -74,6 +74,12 @@ func (controller *Controller) rpcWatch(ctx *gin.Context) responder.Responder { Name: hostProcess.Name, } } + + if tartGuestAgent := target.GetTartGuestAgent(); tartGuestAgent != nil { + watchInstruction.PortForwardAction.Target.TartGuestAgent = &v1.PortForwardTargetTartGuestAgent{ + VMUID: tartGuestAgent.VmUid, + } + } } else { watchInstruction.PortForwardAction.VMUID = typedAction.PortForwardAction.VmUid watchInstruction.PortForwardAction.Port = uint16(typedAction.PortForwardAction.Port) diff --git a/internal/controller/api_vms_exec.go b/internal/controller/api_vms_exec.go index bae7b97..235976e 100644 --- a/internal/controller/api_vms_exec.go +++ b/internal/controller/api_vms_exec.go @@ -212,6 +212,7 @@ func (controller *Controller) newSSHExecSession( vm.UID, 22, "", + "", ) if err != nil { return nil, err diff --git a/internal/controller/api_vms_portforward.go b/internal/controller/api_vms_portforward.go index fc44112..16010f5 100644 --- a/internal/controller/api_vms_portforward.go +++ b/internal/controller/api_vms_portforward.go @@ -13,6 +13,7 @@ import ( "github.com/cirruslabs/orchard/internal/netconncancel" "github.com/cirruslabs/orchard/internal/proxy" "github.com/cirruslabs/orchard/internal/responder" + "github.com/cirruslabs/orchard/pkg/client" v1 "github.com/cirruslabs/orchard/pkg/resource/v1" "github.com/cirruslabs/orchard/rpc" "github.com/coder/websocket" @@ -27,15 +28,27 @@ import ( var errPortForwardRequest = errors.New("failed to request port forwarding") func (controller *Controller) portForwardVM(ctx *gin.Context) responder.Responder { - if responder := controller.authorizeAny(ctx, v1.ServiceAccountRoleComputeWrite, - v1.ServiceAccountRoleComputeConnect); responder != nil { - return responder - } - // Retrieve and parse path and query parameters name := ctx.Param("name") portRaw := ctx.Query("port") hostProcess := ctx.Query("hostProcess") + target := client.PortForwardTarget(ctx.Query("target")) + if target != "" { + if err := target.Validate(); err != nil { + return responder.JSON(http.StatusBadRequest, NewErrorResponse("%v", err)) + } + if portRaw != "" || hostProcess != "" { + return responder.JSON(http.StatusBadRequest, + NewErrorResponse("target cannot be combined with port or hostProcess")) + } + if responder := controller.authorizeAny(ctx, v1.ServiceAccountRoleComputeWrite, + v1.ServiceAccountRoleComputeConnectTartGuestAgent); responder != nil { + return responder + } + } else if responder := controller.authorizeAny(ctx, v1.ServiceAccountRoleComputeWrite, + v1.ServiceAccountRoleComputeConnect); responder != nil { + return responder + } // Host process connections require an additional role var port uint64 @@ -51,7 +64,7 @@ func (controller *Controller) portForwardVM(ctx *gin.Context) responder.Responde if portRaw != "" { return responder.Code(http.StatusBadRequest) } - } else { + } else if target == "" { // VM port forwarding requires a valid non-zero TCP port port, err = strconv.ParseUint(portRaw, 10, 16) if err != nil || port < 1 || port > 65535 { @@ -83,7 +96,7 @@ func (controller *Controller) portForwardVM(ctx *gin.Context) responder.Responde } // Commence port forwarding - return controller.portForward(ctx, waitContext, vm.Worker, vm.UID, uint32(port), hostProcess) + return controller.portForward(ctx, waitContext, vm.Worker, vm.UID, uint32(port), hostProcess, target) } func (controller *Controller) portForward( @@ -93,6 +106,7 @@ func (controller *Controller) portForward( vmUID string, port uint32, hostProcess string, + target client.PortForwardTarget, ) responder.Responder { // Request and wait for a connection with a worker rendezvousConn, err := retry.NewWithData[net.Conn]( @@ -102,7 +116,7 @@ func (controller *Controller) portForward( retry.Attempts(0), retry.LastErrorOnly(true), ).Do(func() (net.Conn, error) { - return controller.portForwardConnection(ctx, notifyContext, workerName, vmUID, port, hostProcess) + return controller.portForwardConnection(ctx, notifyContext, workerName, vmUID, port, hostProcess, target) }) if err != nil { if errors.Is(err, errPortForwardRequest) { @@ -226,6 +240,7 @@ func (controller *Controller) portForwardConnection( vmUID string, port uint32, hostProcess string, + target client.PortForwardTarget, ) (net.Conn, error) { // Create a rendezvous connection point rendezvousCtx, rendezvousCtxCancel := context.WithCancel(ctx) @@ -251,6 +266,12 @@ func (controller *Controller) portForwardConnection( }, }, } + } else if target == client.PortForwardTargetTartGuestAgent { + portForwardAction.Target = &rpc.WatchInstruction_PortForward_Target{ + Value: &rpc.WatchInstruction_PortForward_Target_TartGuestAgent_{ + TartGuestAgent: &rpc.WatchInstruction_PortForward_Target_TartGuestAgent{VmUid: vmUID}, + }, + } } else { portForwardAction.VmUid = vmUID portForwardAction.Port = port diff --git a/internal/controller/api_workers_portforward.go b/internal/controller/api_workers_portforward.go index 71ac1c7..01e183f 100644 --- a/internal/controller/api_workers_portforward.go +++ b/internal/controller/api_workers_portforward.go @@ -50,5 +50,5 @@ func (controller *Controller) portForwardWorker(ctx *gin.Context) responder.Resp } // Commence port-forwarding - return controller.portForward(ctx, waitContext, worker.Name, "", uint32(port), "") + return controller.portForward(ctx, waitContext, worker.Name, "", uint32(port), "", "") } diff --git a/internal/tests/port_forward_test.go b/internal/tests/port_forward_test.go new file mode 100644 index 0000000..92b0a93 --- /dev/null +++ b/internal/tests/port_forward_test.go @@ -0,0 +1,53 @@ +//go:build darwin + +package tests_test + +import ( + "context" + "net" + "testing" + "time" + + "github.com/cirruslabs/orchard/internal/tests/devcontroller" + "github.com/cirruslabs/orchard/internal/tests/platformdependent" + "github.com/cirruslabs/orchard/pkg/client" + guestagent "github.com/cirruslabs/tart-guest-agent/pkg/v1" + "github.com/google/uuid" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials/insecure" +) + +func TestTartGuestAgentPortForward(t *testing.T) { + devClient, _, _ := devcontroller.StartIntegrationTestEnvironment(t) + + // Create a VM with Tart Guest Agent pre-installed + vmName := "test-guest-agent-" + uuid.NewString() + require.NoError(t, devClient.VMs().Create(t.Context(), platformdependent.VM(vmName))) + + // Configure the Tart Guest Agent client to connect through Orchard's port-forwarding API + agentConn, err := grpc.NewClient("passthrough:///tart-guest-agent", + grpc.WithTransportCredentials(insecure.NewCredentials()), + grpc.WithContextDialer(func(_ context.Context, _ string) (net.Conn, error) { + // Keep the stream alive beyond gRPC's temporary dial context + return devClient.VMs().PortForwardTarget(t.Context(), vmName, + client.PortForwardTargetTartGuestAgent, 120) + }), + ) + require.NoError(t, err) + defer agentConn.Close() + + // Perform IP resolution through the forwarded connection + agentClient := guestagent.NewAgentClient(agentConn) + + require.EventuallyWithT(t, func(collect *assert.CollectT) { + rpcContext, cancel := context.WithTimeout(t.Context(), 5*time.Second) + defer cancel() + + response, err := agentClient.ResolveIP(rpcContext, &guestagent.ResolveIPRequest{}, + grpc.WaitForReady(true)) + require.NoError(collect, err) + require.NotNil(collect, net.ParseIP(response.GetIp())) + }, 2*time.Minute, time.Second) +} diff --git a/internal/worker/hostprocess/process.go b/internal/worker/hostprocess/process.go index b003db5..71d0633 100644 --- a/internal/worker/hostprocess/process.go +++ b/internal/worker/hostprocess/process.go @@ -14,6 +14,7 @@ import ( "time" "github.com/avast/retry-go/v4" + "github.com/cirruslabs/orchard/internal/worker/socketalias" v1 "github.com/cirruslabs/orchard/pkg/resource/v1" ) @@ -45,7 +46,7 @@ func NewProcess( // Route the Tart control socket through the runtime directory as well // to work around macOS's 104-byte Unix-domain socket limit - controlSocket, err = shortenControlSocketPath(runtimeDir, controlSocket) + controlSocket, err = socketalias.Create(runtimeDir, controlSocket) if err != nil { return nil, errors.Join(err, os.RemoveAll(runtimeDir)) } @@ -108,24 +109,6 @@ func NewProcess( return process, nil } -func shortenControlSocketPath(runtimeDir string, controlSocketPath string) (string, error) { - // Make the symlink target absolute because a relative TART_HOME would - // otherwise be resolved from runtimeDir, breaking the symlink - absoluteControlSocketPath, err := filepath.Abs(controlSocketPath) - if err != nil { - return "", err - } - - // Place the VM's control socket alias in the runtime directory - aliasControlSocketPath := filepath.Join(runtimeDir, "vm.sock") - - if err := os.Symlink(absoluteControlSocketPath, aliasControlSocketPath); err != nil { - return "", err - } - - return aliasControlSocketPath, nil -} - func (process *Process) Dial(ctx context.Context) (net.Conn, error) { var dialer net.Dialer diff --git a/internal/worker/hostprocess/process_test.go b/internal/worker/hostprocess/process_test.go deleted file mode 100644 index d570c30..0000000 --- a/internal/worker/hostprocess/process_test.go +++ /dev/null @@ -1,58 +0,0 @@ -//nolint:noctx,testpackage,usetesting // Preserve the socket-path tests and their required short /var/tmp paths. -package hostprocess - -import ( - "net" - "os" - "path/filepath" - "strings" - "testing" - - "github.com/stretchr/testify/require" -) - -func TestShortenControlSocketPath(t *testing.T) { - // Create a control socket path that exceeds macOS's 104-byte limit - const controlSocketName = "control.sock" - - vmDir := filepath.Join(t.TempDir(), strings.Repeat("v", 104)) - require.NoError(t, os.MkdirAll(vmDir, 0o700)) - - controlSocketPath := filepath.Join(vmDir, controlSocketName) - require.Greater(t, len(controlSocketPath), 104) - - // Create Orchard's short per-process runtime directory - runtimeDir, err := os.MkdirTemp("/var/tmp", "orchard-hp-test-") - require.NoError(t, err) - t.Cleanup(func() { - require.NoError(t, os.RemoveAll(runtimeDir)) - }) - - // Shorten the control socket path and verify the resulting alias - shortControlSocketPath, err := shortenControlSocketPath(runtimeDir, controlSocketPath) - require.NoError(t, err) - require.Equal(t, filepath.Join(runtimeDir, "vm.sock"), shortControlSocketPath) - require.Less(t, len(shortControlSocketPath), 104) - - // Model Tart binding the socket relative to the long VM directory - t.Chdir(vmDir) - - listener, err := net.Listen("unix", controlSocketName) - require.NoError(t, err) - t.Cleanup(func() { - require.NoError(t, listener.Close()) - }) - - // Model the host process running from Orchard's runtime directory - t.Chdir(runtimeDir) - - // Verify connecting through the long absolute path fails - connection, err := net.Dial("unix", controlSocketPath) - require.Error(t, err) - require.Nil(t, connection) - - // Connect to Tart's control socket through Orchard's short alias - connection, err = net.Dial("unix", shortControlSocketPath) - require.NoError(t, err) - require.NoError(t, connection.Close()) -} diff --git a/internal/worker/rpc.go b/internal/worker/rpc.go index c259bd3..b7274b6 100644 --- a/internal/worker/rpc.go +++ b/internal/worker/rpc.go @@ -67,7 +67,7 @@ func (worker *Worker) watchRPC(ctx context.Context, operationCtx context.Context } } -//nolint:nestif,protogetter // Preserve the original host-process forwarding implementation. +//nolint:nestif,protogetter // Preserve the original forwarding implementation. func (worker *Worker) handlePortForward( ctx context.Context, client rpc.ControllerClient, @@ -98,17 +98,18 @@ func (worker *Worker) handlePortForward( return } - // Retrieve the typed host process target, it's the only possible target right now - hostProcess := target.GetHostProcess() - if hostProcess == nil || hostProcess.VmUid == "" || hostProcess.Name == "" { + switch { + case target.GetTartGuestAgent() != nil: + targetConn, err = worker.dialTartGuestAgent(subCtx, target.GetTartGuestAgent().VmUid) + case target.GetHostProcess() != nil: + targetConn, err = worker.dialHostProcess(subCtx, target.GetHostProcess().VmUid, + target.GetHostProcess().Name) + default: worker.logger.Warn("port forwarding failed: invalid or unsupported target") return } - - // Dial host process - targetConn, err = worker.dialHostProcess(ctx, hostProcess.VmUid, hostProcess.Name) if err != nil { - worker.logger.Warnf("port forwarding failed: failed to connect to host process: %v", err) + worker.logger.Warnf("port forwarding failed: %v", err) return } } else { @@ -155,6 +156,11 @@ func (worker *Worker) handlePortForward( } } + // Close the target on return or cancellation to unblock pending reads and writes + defer targetConn.Close() + stopClosing := context.AfterFunc(subCtx, func() { _ = targetConn.Close() }) + defer stopClosing() + // Proxy bytes grpcConn := &grpc_net_conn.Conn{ Stream: stream, diff --git a/internal/worker/rpcv2.go b/internal/worker/rpcv2.go index 0f6cd51..ba344d8 100644 --- a/internal/worker/rpcv2.go +++ b/internal/worker/rpcv2.go @@ -6,6 +6,7 @@ import ( "net" "github.com/cirruslabs/orchard/internal/proxy" + "github.com/cirruslabs/orchard/internal/worker/socketalias" "github.com/cirruslabs/orchard/internal/worker/vmmanager" v1 "github.com/cirruslabs/orchard/pkg/resource/v1" "github.com/samber/lo" @@ -48,6 +49,11 @@ func (worker *Worker) handlePortForwardV2(ctx context.Context, portForward *v1.P errorMessage = fmt.Sprintf("port-forwarding failed: %v", err) worker.logger.Warn(errorMessage) + } else { + // Close the target on return or cancellation to unblock pending reads and writes + defer vmConn.Close() + stopClosing := context.AfterFunc(ctx, func() { _ = vmConn.Close() }) + defer stopClosing() } // Respond @@ -82,13 +88,14 @@ func (worker *Worker) handlePortForwardV2Inner( return nil, fmt.Errorf("target and legacy fields are mutually exclusive") } - // Retrieve the typed host process target, it's the only possible target right now - if target.HostProcess == nil || target.HostProcess.VMUID == "" || target.HostProcess.Name == "" { + switch { + case target.TartGuestAgent != nil: + return worker.dialTartGuestAgent(ctx, target.TartGuestAgent.VMUID) + case target.HostProcess != nil: + return worker.dialHostProcess(ctx, target.HostProcess.VMUID, target.HostProcess.Name) + default: return nil, fmt.Errorf("invalid or unsupported target") } - - // Dial host process - return worker.dialHostProcess(ctx, target.HostProcess.VMUID, target.HostProcess.Name) } var host string @@ -133,6 +140,40 @@ func (worker *Worker) handlePortForwardV2Inner( return vmConn, nil } +//nolint:err113,perfsprint // runtime and VM state failures are reported to the port-forward caller +func (worker *Worker) dialTartGuestAgent(ctx context.Context, vmUID string) (net.Conn, error) { + // Validate the VM UID and runtime + if vmUID == "" { + return nil, fmt.Errorf("invalid Tart Guest Agent target: VM UID is required") + } + + if worker.runtime.ID() != v1.RuntimeTart || worker.runtime.Synthetic() { + return nil, fmt.Errorf("forwarding to Tart Guest Agent requires the Tart runtime") + } + + // Find the running VM + vm, err := worker.findVMByUID(vmUID) + if err != nil { + return nil, err + } + if !vm.Running() { + return nil, fmt.Errorf("VM with UID %q is not running", vmUID) + } + + // Connect to Tart Guest Agent through the VM's control socket + path, err := vm.OnDiskName().ControlSocketPath() + if err != nil { + return nil, err + } + + conn, err := socketalias.DialContext(ctx, path) + if err != nil { + return nil, fmt.Errorf("failed to connect to Tart Guest Agent: %w", err) + } + + return conn, nil +} + func (worker *Worker) handleGetIPV2(ctx context.Context, resolveIP *v1.ResolveIPAction) { var errorMessage string @@ -192,7 +233,12 @@ func (worker *Worker) findVMByUID(uid string) (vmmanager.VM, error) { return vm, nil } +//nolint:err113,perfsprint // Use descriptive errors for host process validation func (worker *Worker) dialHostProcess(ctx context.Context, vmUID string, name string) (net.Conn, error) { + if vmUID == "" || name == "" { + return nil, fmt.Errorf("invalid host process target: VM UID and name are required") + } + vm, err := worker.findVMByUID(vmUID) if err != nil { return nil, err diff --git a/internal/worker/socketalias/socketalias.go b/internal/worker/socketalias/socketalias.go new file mode 100644 index 0000000..75dc6e1 --- /dev/null +++ b/internal/worker/socketalias/socketalias.go @@ -0,0 +1,67 @@ +package socketalias + +import ( + "context" + "errors" + "net" + "os" + "path/filepath" +) + +// baseDirectory is the parent directory for transient socket aliases. +// +// To keep the Unix socket path short enough and fit the platform limit, +// we're specifically requesting "/var/tmp" instead of "/var/folders/.../T/". +const baseDirectory = "/var/tmp" + +// Create creates a short Unix socket alias in runtimeDir that points to socketPath. +// +// The alias remains valid until runtimeDir is removed. +func Create(runtimeDir string, socketPath string) (string, error) { + // Make the symlink target absolute because a relative TART_HOME would + // otherwise be resolved from runtimeDir, breaking the symlink + absoluteSocketPath, err := filepath.Abs(socketPath) + if err != nil { + return "", err + } + + // Place the VM's control socket alias in the runtime directory + aliasSocketPath := filepath.Join(runtimeDir, "vm.sock") + if err := os.Symlink(absoluteSocketPath, aliasSocketPath); err != nil { + return "", err + } + + return aliasSocketPath, nil +} + +// DialContext connects to socketPath through a short transient alias. +func DialContext(ctx context.Context, socketPath string) (net.Conn, error) { + // Create a directory where the transient Unix socket alias will live + runtimeDir, err := os.MkdirTemp(baseDirectory, "orchard-socket-") + if err != nil { + return nil, err + } + + // Route the socket through the short runtime directory + aliasSocketPath, err := Create(runtimeDir, socketPath) + if err != nil { + return nil, errors.Join(err, os.RemoveAll(runtimeDir)) + } + + // Connect through the short alias + var dialer net.Dialer + conn, err := dialer.DialContext(ctx, "unix", aliasSocketPath) + + // Remove the transient alias after the connection attempt + cleanupErr := os.RemoveAll(runtimeDir) + if err != nil { + return nil, errors.Join(err, cleanupErr) + } + + // Avoid returning a live connection when its alias cannot be removed + if cleanupErr != nil { + return nil, errors.Join(cleanupErr, conn.Close()) + } + + return conn, nil +} diff --git a/internal/worker/socketalias/socketalias_test.go b/internal/worker/socketalias/socketalias_test.go new file mode 100644 index 0000000..fb06c5d --- /dev/null +++ b/internal/worker/socketalias/socketalias_test.go @@ -0,0 +1,49 @@ +package socketalias_test + +import ( + "net" + "os" + "path/filepath" + "strings" + "testing" + + "github.com/cirruslabs/orchard/internal/worker/socketalias" + "github.com/stretchr/testify/require" + "golang.org/x/sys/unix" +) + +func TestDialContextWithLongPath(t *testing.T) { + const ( + socketName = "control.sock" + unixSocketPathLimit = len(unix.RawSockaddrUnix{}.Path) + doubleUnixSocketPathLimit = 2 * unixSocketPathLimit + ) + + // Create the socket at an overlong absolute path without passing that path to bind + originalWorkingDirectory, err := os.Getwd() + require.NoError(t, err) + + socketDir := filepath.Join(t.TempDir(), strings.Repeat("x", doubleUnixSocketPathLimit)) + require.NoError(t, os.MkdirAll(socketDir, 0o700)) + socketPath := filepath.Join(socketDir, socketName) + + t.Chdir(socketDir) + + listener, err := (&net.ListenConfig{}).Listen(t.Context(), "unix", socketName) + require.NoError(t, err) + t.Cleanup(func() { + require.NoError(t, listener.Close()) + }) + + t.Chdir(originalWorkingDirectory) + + // Verify that connecting through the long absolute path fails + connection, err := (&net.Dialer{}).DialContext(t.Context(), "unix", socketPath) + require.Error(t, err) + require.Nil(t, connection) + + // Verify that connecting through a short transient alias succeeds + connection, err = socketalias.DialContext(t.Context(), socketPath) + require.NoError(t, err) + require.NoError(t, connection.Close()) +} diff --git a/pkg/client/vms.go b/pkg/client/vms.go index 096e8b9..d827209 100644 --- a/pkg/client/vms.go +++ b/pkg/client/vms.go @@ -13,6 +13,21 @@ import ( "github.com/coder/websocket" ) +type PortForwardTarget string + +const ( + PortForwardTargetTartGuestAgent PortForwardTarget = "tart-guest-agent" +) + +func (target PortForwardTarget) Validate() error { + switch target { + case PortForwardTargetTartGuestAgent: + return nil + default: + return fmt.Errorf("unsupported port-forward target %q", target) + } +} + type VMsService struct { client *Client } @@ -182,6 +197,19 @@ func (service *VMsService) PortForwardHostProcess( }) } +func (service *VMsService) PortForwardTarget( + ctx context.Context, + name string, + target PortForwardTarget, + waitSeconds uint16, +) (net.Conn, error) { + return service.client.wsRequest(ctx, fmt.Sprintf("vms/%s/port-forward", url.PathEscape(name)), + map[string]string{ + "target": string(target), + "wait": strconv.FormatUint(uint64(waitSeconds), 10), + }) +} + func (service *VMsService) Exec( ctx context.Context, name string, diff --git a/pkg/resource/v1/service_account_role.go b/pkg/resource/v1/service_account_role.go index 17be749..4dd6201 100644 --- a/pkg/resource/v1/service_account_role.go +++ b/pkg/resource/v1/service_account_role.go @@ -10,13 +10,14 @@ var ErrUnsupportedServiceAccountRole = errors.New("unsupported service account r type ServiceAccountRole string const ( - ServiceAccountRoleComputeRead ServiceAccountRole = "compute:read" - ServiceAccountRoleComputeWrite ServiceAccountRole = "compute:write" - ServiceAccountRoleComputeConnect ServiceAccountRole = "compute:connect" - ServiceAccountRoleHostProcessWrite ServiceAccountRole = "host-process:write" - ServiceAccountRoleHostProcessConnect ServiceAccountRole = "host-process:connect" - ServiceAccountRoleAdminRead ServiceAccountRole = "admin:read" - ServiceAccountRoleAdminWrite ServiceAccountRole = "admin:write" + ServiceAccountRoleComputeRead ServiceAccountRole = "compute:read" + ServiceAccountRoleComputeWrite ServiceAccountRole = "compute:write" + ServiceAccountRoleComputeConnect ServiceAccountRole = "compute:connect" + ServiceAccountRoleComputeConnectTartGuestAgent ServiceAccountRole = "compute:connect:tart-guest-agent" + ServiceAccountRoleHostProcessWrite ServiceAccountRole = "host-process:write" + ServiceAccountRoleHostProcessConnect ServiceAccountRole = "host-process:connect" + ServiceAccountRoleAdminRead ServiceAccountRole = "admin:read" + ServiceAccountRoleAdminWrite ServiceAccountRole = "admin:write" ) func NewServiceAccountRole(name string) (ServiceAccountRole, error) { @@ -27,6 +28,8 @@ func NewServiceAccountRole(name string) (ServiceAccountRole, error) { return ServiceAccountRoleComputeWrite, nil case string(ServiceAccountRoleComputeConnect): return ServiceAccountRoleComputeConnect, nil + case string(ServiceAccountRoleComputeConnectTartGuestAgent): + return ServiceAccountRoleComputeConnectTartGuestAgent, nil case string(ServiceAccountRoleHostProcessWrite): return ServiceAccountRoleHostProcessWrite, nil case string(ServiceAccountRoleHostProcessConnect): @@ -45,6 +48,7 @@ func AllServiceAccountRoles() []ServiceAccountRole { ServiceAccountRoleComputeRead, ServiceAccountRoleComputeWrite, ServiceAccountRoleComputeConnect, + ServiceAccountRoleComputeConnectTartGuestAgent, ServiceAccountRoleHostProcessWrite, ServiceAccountRoleHostProcessConnect, ServiceAccountRoleAdminRead, diff --git a/pkg/resource/v1/watch_instruction.go b/pkg/resource/v1/watch_instruction.go index 0c3ca7f..4f69256 100644 --- a/pkg/resource/v1/watch_instruction.go +++ b/pkg/resource/v1/watch_instruction.go @@ -15,7 +15,8 @@ type PortForwardAction struct { } type PortForwardTarget struct { - HostProcess *PortForwardTargetHostProcess `json:"hostProcess,omitempty"` + HostProcess *PortForwardTargetHostProcess `json:"hostProcess,omitempty"` + TartGuestAgent *PortForwardTargetTartGuestAgent `json:"tartGuestAgent,omitempty"` } type PortForwardTargetHostProcess struct { @@ -23,6 +24,10 @@ type PortForwardTargetHostProcess struct { Name string `json:"name"` } +type PortForwardTargetTartGuestAgent struct { + VMUID string `json:"vmUID"` +} + type SyncVMsAction struct { // nothing for now } diff --git a/rpc/orchard.pb.go b/rpc/orchard.pb.go index 0b54453..bd85720 100644 --- a/rpc/orchard.pb.go +++ b/rpc/orchard.pb.go @@ -224,7 +224,7 @@ type WatchInstruction_PortForward struct { VmUid string `protobuf:"bytes,2,opt,name=vm_uid,json=vmUid,proto3" json:"vm_uid,omitempty"` Port uint32 `protobuf:"varint,3,opt,name=port,proto3" json:"port,omitempty"` // Typed alternative to the legacy port-forwarding destination, - // adding support for host processes + // adding support for host processes and Tart Guest Agent Target *WatchInstruction_PortForward_Target `protobuf:"bytes,4,opt,name=target,proto3" json:"target,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache @@ -384,6 +384,7 @@ type WatchInstruction_PortForward_Target struct { // Types that are valid to be assigned to Value: // // *WatchInstruction_PortForward_Target_HostProcess_ + // *WatchInstruction_PortForward_Target_TartGuestAgent_ Value isWatchInstruction_PortForward_Target_Value `protobuf_oneof:"value"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache @@ -435,6 +436,15 @@ func (x *WatchInstruction_PortForward_Target) GetHostProcess() *WatchInstruction return nil } +func (x *WatchInstruction_PortForward_Target) GetTartGuestAgent() *WatchInstruction_PortForward_Target_TartGuestAgent { + if x != nil { + if x, ok := x.Value.(*WatchInstruction_PortForward_Target_TartGuestAgent_); ok { + return x.TartGuestAgent + } + } + return nil +} + type isWatchInstruction_PortForward_Target_Value interface { isWatchInstruction_PortForward_Target_Value() } @@ -443,9 +453,16 @@ type WatchInstruction_PortForward_Target_HostProcess_ struct { HostProcess *WatchInstruction_PortForward_Target_HostProcess `protobuf:"bytes,1,opt,name=host_process,json=hostProcess,proto3,oneof"` } +type WatchInstruction_PortForward_Target_TartGuestAgent_ struct { + TartGuestAgent *WatchInstruction_PortForward_Target_TartGuestAgent `protobuf:"bytes,2,opt,name=tart_guest_agent,json=tartGuestAgent,proto3,oneof"` +} + func (*WatchInstruction_PortForward_Target_HostProcess_) isWatchInstruction_PortForward_Target_Value() { } +func (*WatchInstruction_PortForward_Target_TartGuestAgent_) isWatchInstruction_PortForward_Target_Value() { +} + // Forward the byte stream to a host process, identified by // a VM UID and a host process name type WatchInstruction_PortForward_Target_HostProcess struct { @@ -500,25 +517,73 @@ func (x *WatchInstruction_PortForward_Target_HostProcess) GetName() string { return "" } +// Forward the byte stream to the Tart Guest Agent of a VM +type WatchInstruction_PortForward_Target_TartGuestAgent struct { + state protoimpl.MessageState `protogen:"open.v1"` + VmUid string `protobuf:"bytes,1,opt,name=vm_uid,json=vmUid,proto3" json:"vm_uid,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *WatchInstruction_PortForward_Target_TartGuestAgent) Reset() { + *x = WatchInstruction_PortForward_Target_TartGuestAgent{} + mi := &file_orchard_proto_msgTypes[8] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *WatchInstruction_PortForward_Target_TartGuestAgent) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*WatchInstruction_PortForward_Target_TartGuestAgent) ProtoMessage() {} + +func (x *WatchInstruction_PortForward_Target_TartGuestAgent) ProtoReflect() protoreflect.Message { + mi := &file_orchard_proto_msgTypes[8] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use WatchInstruction_PortForward_Target_TartGuestAgent.ProtoReflect.Descriptor instead. +func (*WatchInstruction_PortForward_Target_TartGuestAgent) Descriptor() ([]byte, []int) { + return file_orchard_proto_rawDescGZIP(), []int{0, 0, 0, 1} +} + +func (x *WatchInstruction_PortForward_Target_TartGuestAgent) GetVmUid() string { + if x != nil { + return x.VmUid + } + return "" +} + var File_orchard_proto protoreflect.FileDescriptor const file_orchard_proto_rawDesc = "" + "\n" + - "\rorchard.proto\x1a\x1bgoogle/protobuf/empty.proto\"\xfe\x04\n" + + "\rorchard.proto\x1a\x1bgoogle/protobuf/empty.proto\"\x88\x06\n" + "\x10WatchInstruction\x12O\n" + "\x13port_forward_action\x18\x01 \x01(\v2\x1d.WatchInstruction.PortForwardH\x00R\x11portForwardAction\x12C\n" + "\x0fsync_vms_action\x18\x02 \x01(\v2\x19.WatchInstruction.SyncVMsH\x00R\rsyncVmsAction\x12I\n" + - "\x11resolve_ip_action\x18\x03 \x01(\v2\x1b.WatchInstruction.ResolveIPH\x00R\x0fresolveIpAction\x1a\xb5\x02\n" + + "\x11resolve_ip_action\x18\x03 \x01(\v2\x1b.WatchInstruction.ResolveIPH\x00R\x0fresolveIpAction\x1a\xbf\x03\n" + "\vPortForward\x12\x18\n" + "\asession\x18\x01 \x01(\tR\asession\x12\x15\n" + "\x06vm_uid\x18\x02 \x01(\tR\x05vmUid\x12\x12\n" + "\x04port\x18\x03 \x01(\rR\x04port\x12<\n" + - "\x06target\x18\x04 \x01(\v2$.WatchInstruction.PortForward.TargetR\x06target\x1a\xa2\x01\n" + + "\x06target\x18\x04 \x01(\v2$.WatchInstruction.PortForward.TargetR\x06target\x1a\xac\x02\n" + "\x06Target\x12U\n" + - "\fhost_process\x18\x01 \x01(\v20.WatchInstruction.PortForward.Target.HostProcessH\x00R\vhostProcess\x1a8\n" + + "\fhost_process\x18\x01 \x01(\v20.WatchInstruction.PortForward.Target.HostProcessH\x00R\vhostProcess\x12_\n" + + "\x10tart_guest_agent\x18\x02 \x01(\v23.WatchInstruction.PortForward.Target.TartGuestAgentH\x00R\x0etartGuestAgent\x1a8\n" + "\vHostProcess\x12\x15\n" + "\x06vm_uid\x18\x01 \x01(\tR\x05vmUid\x12\x12\n" + - "\x04name\x18\x02 \x01(\tR\x04nameB\a\n" + + "\x04name\x18\x02 \x01(\tR\x04name\x1a'\n" + + "\x0eTartGuestAgent\x12\x15\n" + + "\x06vm_uid\x18\x01 \x01(\tR\x05vmUidB\a\n" + "\x05value\x1a\t\n" + "\aSyncVMs\x1a<\n" + "\tResolveIP\x12\x18\n" + @@ -548,17 +613,18 @@ func file_orchard_proto_rawDescGZIP() []byte { return file_orchard_proto_rawDescData } -var file_orchard_proto_msgTypes = make([]protoimpl.MessageInfo, 8) +var file_orchard_proto_msgTypes = make([]protoimpl.MessageInfo, 9) var file_orchard_proto_goTypes = []any{ - (*WatchInstruction)(nil), // 0: WatchInstruction - (*PortForwardData)(nil), // 1: PortForwardData - (*ResolveIPResult)(nil), // 2: ResolveIPResult - (*WatchInstruction_PortForward)(nil), // 3: WatchInstruction.PortForward - (*WatchInstruction_SyncVMs)(nil), // 4: WatchInstruction.SyncVMs - (*WatchInstruction_ResolveIP)(nil), // 5: WatchInstruction.ResolveIP - (*WatchInstruction_PortForward_Target)(nil), // 6: WatchInstruction.PortForward.Target - (*WatchInstruction_PortForward_Target_HostProcess)(nil), // 7: WatchInstruction.PortForward.Target.HostProcess - (*emptypb.Empty)(nil), // 8: google.protobuf.Empty + (*WatchInstruction)(nil), // 0: WatchInstruction + (*PortForwardData)(nil), // 1: PortForwardData + (*ResolveIPResult)(nil), // 2: ResolveIPResult + (*WatchInstruction_PortForward)(nil), // 3: WatchInstruction.PortForward + (*WatchInstruction_SyncVMs)(nil), // 4: WatchInstruction.SyncVMs + (*WatchInstruction_ResolveIP)(nil), // 5: WatchInstruction.ResolveIP + (*WatchInstruction_PortForward_Target)(nil), // 6: WatchInstruction.PortForward.Target + (*WatchInstruction_PortForward_Target_HostProcess)(nil), // 7: WatchInstruction.PortForward.Target.HostProcess + (*WatchInstruction_PortForward_Target_TartGuestAgent)(nil), // 8: WatchInstruction.PortForward.Target.TartGuestAgent + (*emptypb.Empty)(nil), // 9: google.protobuf.Empty } var file_orchard_proto_depIdxs = []int32{ 3, // 0: WatchInstruction.port_forward_action:type_name -> WatchInstruction.PortForward @@ -566,17 +632,18 @@ var file_orchard_proto_depIdxs = []int32{ 5, // 2: WatchInstruction.resolve_ip_action:type_name -> WatchInstruction.ResolveIP 6, // 3: WatchInstruction.PortForward.target:type_name -> WatchInstruction.PortForward.Target 7, // 4: WatchInstruction.PortForward.Target.host_process:type_name -> WatchInstruction.PortForward.Target.HostProcess - 8, // 5: Controller.Watch:input_type -> google.protobuf.Empty - 1, // 6: Controller.PortForward:input_type -> PortForwardData - 2, // 7: Controller.ResolveIP:input_type -> ResolveIPResult - 0, // 8: Controller.Watch:output_type -> WatchInstruction - 1, // 9: Controller.PortForward:output_type -> PortForwardData - 8, // 10: Controller.ResolveIP:output_type -> google.protobuf.Empty - 8, // [8:11] is the sub-list for method output_type - 5, // [5:8] is the sub-list for method input_type - 5, // [5:5] is the sub-list for extension type_name - 5, // [5:5] is the sub-list for extension extendee - 0, // [0:5] is the sub-list for field type_name + 8, // 5: WatchInstruction.PortForward.Target.tart_guest_agent:type_name -> WatchInstruction.PortForward.Target.TartGuestAgent + 9, // 6: Controller.Watch:input_type -> google.protobuf.Empty + 1, // 7: Controller.PortForward:input_type -> PortForwardData + 2, // 8: Controller.ResolveIP:input_type -> ResolveIPResult + 0, // 9: Controller.Watch:output_type -> WatchInstruction + 1, // 10: Controller.PortForward:output_type -> PortForwardData + 9, // 11: Controller.ResolveIP:output_type -> google.protobuf.Empty + 9, // [9:12] is the sub-list for method output_type + 6, // [6:9] is the sub-list for method input_type + 6, // [6:6] is the sub-list for extension type_name + 6, // [6:6] is the sub-list for extension extendee + 0, // [0:6] is the sub-list for field type_name } func init() { file_orchard_proto_init() } @@ -591,6 +658,7 @@ func file_orchard_proto_init() { } file_orchard_proto_msgTypes[6].OneofWrappers = []any{ (*WatchInstruction_PortForward_Target_HostProcess_)(nil), + (*WatchInstruction_PortForward_Target_TartGuestAgent_)(nil), } type x struct{} out := protoimpl.TypeBuilder{ @@ -598,7 +666,7 @@ func file_orchard_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_orchard_proto_rawDesc), len(file_orchard_proto_rawDesc)), NumEnums: 0, - NumMessages: 8, + NumMessages: 9, NumExtensions: 0, NumServices: 1, }, diff --git a/rpc/orchard.proto b/rpc/orchard.proto index ca11658..e11f8ef 100644 --- a/rpc/orchard.proto +++ b/rpc/orchard.proto @@ -27,8 +27,14 @@ message WatchInstruction { string name = 2; } + // Forward the byte stream to the Tart Guest Agent running inside the VM + message TartGuestAgent { + string vm_uid = 1; + } + oneof value { HostProcess host_process = 1; + TartGuestAgent tart_guest_agent = 2; } } @@ -40,7 +46,7 @@ message WatchInstruction { uint32 port = 3; // Typed alternative to the legacy port-forwarding destination, - // adding support for host processes + // adding support for host processes and Tart Guest Agent Target target = 4; } message SyncVMs {