Skip to content

Commit d75d8bd

Browse files
authored
Add protocol/grpc helper for plaintext gRPC exploits (#588)
1 parent 28c657d commit d75d8bd

4 files changed

Lines changed: 339 additions & 1 deletion

File tree

go.mod

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ require (
1414
golang.org/x/net v0.54.0
1515
golang.org/x/sys v0.44.0
1616
golang.org/x/text v0.37.0
17+
google.golang.org/grpc v1.81.0
1718
modernc.org/sqlite v1.50.1
1819
)
1920

@@ -28,6 +29,8 @@ require (
2829
github.com/ncruces/go-strftime v1.0.0 // indirect
2930
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
3031
golang.org/x/sync v0.20.0 // indirect
32+
google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 // indirect
33+
google.golang.org/protobuf v1.36.11 // indirect
3134
modernc.org/libc v1.72.3 // indirect
3235
modernc.org/mathutil v1.7.1 // indirect
3336
modernc.org/memory v1.11.0 // indirect

go.sum

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,18 @@ github.com/antchfx/htmlquery v1.3.6 h1:RNHHL7YehO5XdO8IM8CynwLKONwRHWkrghbYhQIk9
44
github.com/antchfx/htmlquery v1.3.6/go.mod h1:kcVUqancxPygm26X2rceEcagZFFVkLEE7xgLkGSDl/4=
55
github.com/antchfx/xpath v1.3.6 h1:s0y+ElRRtTQdfHP609qFu0+c6bglDv20pqOViQjjdPI=
66
github.com/antchfx/xpath v1.3.6/go.mod h1:i54GszH55fYfBmoZXapTHN8T8tkcHfRgLyVwwqzXNcs=
7+
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
8+
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
79
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
810
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
911
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
1012
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
1113
github.com/emiago/sipgo v1.1.2 h1:JvLqEvqNSQm2mBX40qZ7O0WC3Ee67Z0UrfmBI7y6Beo=
1214
github.com/emiago/sipgo v1.1.2/go.mod h1:DuwAxBZhKMqIzQFPGZb1MVAGU6Wuxj64oTOhd5dx/FY=
15+
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
16+
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
17+
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
18+
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
1319
github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU=
1420
github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM=
1521
github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og=
@@ -18,8 +24,11 @@ github.com/gobwas/ws v1.4.0 h1:CTaoG1tojrh4ucGPcoJFiAQUAsEWekEWvLy7GsVNqGs=
1824
github.com/gobwas/ws v1.4.0/go.mod h1:G3gNqMNtPppf5XUz7O4shetPpcZ1VJ7zt18dlUeakrc=
1925
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
2026
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
21-
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
27+
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
28+
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
2229
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
30+
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
31+
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
2332
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs=
2433
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
2534
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
@@ -44,6 +53,18 @@ github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8
4453
github.com/vjeantet/ldapserver v1.0.2-0.20240305064909-a417792e2906 h1:qHFp1iRg6qE8xYel3bQT9x70pyxsdPLbJnM40HG3Oig=
4554
github.com/vjeantet/ldapserver v1.0.2-0.20240305064909-a417792e2906/go.mod h1:YvUqhu5vYhmbcLReMLrm/Tq3S7Yj43kSVFvvol6Lh6k=
4655
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
56+
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
57+
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
58+
go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I=
59+
go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0=
60+
go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM=
61+
go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY=
62+
go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg=
63+
go.opentelemetry.io/otel/sdk v1.43.0/go.mod h1:P+IkVU3iWukmiit/Yf9AWvpyRDlUeBaRg6Y+C58QHzg=
64+
go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfCGLEo89fDkw=
65+
go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A=
66+
go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A=
67+
go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0=
4768
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
4869
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
4970
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
@@ -122,6 +143,14 @@ golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxb
122143
golang.org/x/tools v0.44.0 h1:UP4ajHPIcuMjT1GqzDWRlalUEoY+uzoZKnhOjbIPD2c=
123144
golang.org/x/tools v0.44.0/go.mod h1:KA0AfVErSdxRZIsOVipbv3rQhVXTnlU6UhKxHd1seDI=
124145
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
146+
gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4=
147+
gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E=
148+
google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 h1:ggcbiqK8WWh6l1dnltU4BgWGIGo+EVYxCaAPih/zQXQ=
149+
google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
150+
google.golang.org/grpc v1.81.0 h1:W3G9N3KQf3BU+YuCtGKJk0CmxQNbAISICD/9AORxLIw=
151+
google.golang.org/grpc v1.81.0/go.mod h1:xGH9GfzOyMTGIOXBJmXt+BX/V0kcdQbdcuwQ/zNw42I=
152+
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
153+
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
125154
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
126155
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
127156
gotest.tools/v3 v3.5.1 h1:EENdUnS3pdur5nybKYIh2Vfgc8IUNBjxDPSjtiJcOzU=

protocol/grpc/grpc.go

Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
// Package grpc helps exploit modules talk to plaintext (h2c) or TLS
2+
// gRPC services. Two layers:
3+
//
4+
// - Invoke is the fast path for unary RPCs: one request, one response,
5+
// no grpc-go imports on the caller side.
6+
//
7+
// body := grpc.EncodeBytesField(1, payload)
8+
// out, ok := grpc.Invoke(host, port, "/svc/Method", body, 5, conf.SSL)
9+
//
10+
// - Dial returns a *grpc.ClientConn pre-configured with the raw codec
11+
// and transport. Use it when Invoke is not enough: server- /
12+
// client- / bidi-streaming RPCs, gRPC reflection, or reusing one
13+
// connection across many calls. The caller drives the standard
14+
// grpc-go ClientStream API and is responsible for closing the conn.
15+
//
16+
// EncodeBytesField / EncodeStringField wrap a value as a proto3
17+
// length-delimited field so callers can build proto messages by hand on
18+
// the simple cases. For complex schemas, callers bring their own
19+
// protoc-generated marshallers and pass the resulting bytes through.
20+
package grpc
21+
22+
import (
23+
"context"
24+
"crypto/tls"
25+
"errors"
26+
"fmt"
27+
"net"
28+
"strconv"
29+
"time"
30+
31+
"github.com/vulncheck-oss/go-exploit/output"
32+
"google.golang.org/grpc"
33+
"google.golang.org/grpc/credentials"
34+
"google.golang.org/grpc/credentials/insecure"
35+
"google.golang.org/grpc/encoding"
36+
)
37+
38+
const rawCodecName = "vulncheck-raw-bytes"
39+
40+
// rawCodec passes pre-encoded protobuf bytes through grpc-go untouched
41+
// so callers can hand-roll proto3 wire format without protoc.
42+
type rawCodec struct{}
43+
44+
var errCodecType = errors.New("grpc rawCodec: unexpected value type")
45+
46+
func (rawCodec) Marshal(v any) ([]byte, error) {
47+
b, ok := v.([]byte)
48+
if !ok {
49+
return nil, fmt.Errorf("%w: %T (want []byte)", errCodecType, v)
50+
}
51+
52+
return b, nil
53+
}
54+
55+
func (rawCodec) Unmarshal(data []byte, v any) error {
56+
out, ok := v.(*[]byte)
57+
if !ok {
58+
return fmt.Errorf("%w: %T (want *[]byte)", errCodecType, v)
59+
}
60+
*out = data
61+
62+
return nil
63+
}
64+
65+
func (rawCodec) Name() string { return rawCodecName }
66+
67+
// grpc-go's codec registry is process-wide, so registration must be
68+
// package-level.
69+
//
70+
//nolint:gochecknoinits
71+
func init() {
72+
encoding.RegisterCodec(rawCodec{})
73+
}
74+
75+
// Dial opens a gRPC client connection with the package's raw codec and
76+
// the chosen transport. ssl picks h2c (false) or TLS (true); under TLS
77+
// the client skips certificate verification (exploit targets routinely
78+
// use self-signed certs). The caller is responsible for Close. Returns
79+
// ok=false on any error from grpc.NewClient; the underlying error is
80+
// logged at framework-debug level.
81+
func Dial(host string, port int, ssl bool) (*grpc.ClientConn, bool) {
82+
addr := net.JoinHostPort(host, strconv.Itoa(port))
83+
conn, err := grpc.NewClient(addr,
84+
grpc.WithTransportCredentials(transportCreds(ssl)),
85+
grpc.WithDefaultCallOptions(grpc.ForceCodec(rawCodec{})),
86+
)
87+
if err != nil {
88+
output.PrintfFrameworkDebug("grpc.NewClient(%s): %s", addr, err)
89+
90+
return nil, false
91+
}
92+
93+
return conn, true
94+
}
95+
96+
// Invoke runs a unary RPC with timeoutSec as the dial-and-invoke
97+
// budget. Returns the response body or ok=false on any failure; the
98+
// underlying error is logged at framework-debug level with the method,
99+
// host and port so failed RPCs are attributable in a multi-target run.
100+
func Invoke(host string, port int, method string, in []byte, timeoutSec int, ssl bool) ([]byte, bool) {
101+
conn, ok := Dial(host, port, ssl)
102+
if !ok {
103+
return nil, false
104+
}
105+
defer conn.Close()
106+
107+
ctx, cancel := context.WithTimeout(context.Background(),
108+
time.Duration(timeoutSec)*time.Second)
109+
defer cancel()
110+
111+
var out []byte
112+
if err := conn.Invoke(ctx, method, in, &out); err != nil {
113+
output.PrintfFrameworkDebug("grpc Invoke %s on %s:%d: %s", method, host, port, err)
114+
115+
return nil, false
116+
}
117+
118+
return out, true
119+
}
120+
121+
// transportCreds picks the credential option for the requested
122+
// transport. The TLS config mirrors protocol/httphelper.go: certificate
123+
// verification is intentionally disabled and the minimum version is
124+
// permissive. We have no control over the SSL versions supported on the
125+
// remote target. Be permissive for more targets.
126+
func transportCreds(ssl bool) credentials.TransportCredentials {
127+
if ssl {
128+
return credentials.NewTLS(&tls.Config{
129+
InsecureSkipVerify: true, //nolint:gosec
130+
MinVersion: tls.VersionSSL30, //nolint:staticcheck
131+
})
132+
}
133+
134+
return insecure.NewCredentials()
135+
}
136+
137+
// EncodeBytesField wraps value as a proto3 length-delimited field
138+
// (wire type 2). Lets modules skip protoc on messages with one or two
139+
// fields.
140+
func EncodeBytesField(fieldNumber uint32, value []byte) []byte {
141+
out := appendVarint(nil, uint64(fieldNumber<<3|2))
142+
out = appendVarint(out, uint64(len(value)))
143+
144+
return append(out, value...)
145+
}
146+
147+
// EncodeStringField wraps value as a proto3 string field (same wire
148+
// shape as bytes).
149+
func EncodeStringField(fieldNumber uint32, value string) []byte {
150+
return EncodeBytesField(fieldNumber, []byte(value))
151+
}
152+
153+
func appendVarint(b []byte, v uint64) []byte {
154+
for v >= 0x80 {
155+
b = append(b, byte(v|0x80))
156+
v >>= 7
157+
}
158+
159+
return append(b, byte(v))
160+
}

protocol/grpc/grpc_test.go

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
package grpc_test
2+
3+
import (
4+
"context"
5+
"encoding/hex"
6+
"fmt"
7+
"net"
8+
"strconv"
9+
"testing"
10+
11+
vcgrpc "github.com/vulncheck-oss/go-exploit/protocol/grpc"
12+
"google.golang.org/grpc"
13+
)
14+
15+
// hexEqual asserts got matches expected hex.
16+
func hexEqual(t *testing.T, got []byte, expected string) {
17+
t.Helper()
18+
if hex.EncodeToString(got) != expected {
19+
t.Fatalf("hex mismatch:\n got %x\n expected %s", got, expected)
20+
}
21+
}
22+
23+
// Each fixture row: tag = (fieldNumber<<3)|2, then varint length, then
24+
// payload bytes. Verified once against protoc output during development
25+
// and frozen here so the suite has no Python / protoc dependency.
26+
func TestEncodeBytesField(t *testing.T) {
27+
cases := []struct {
28+
name string
29+
fieldNumber uint32
30+
value []byte
31+
expected string
32+
}{
33+
{"empty", 1, nil, "0a00"},
34+
{"short", 1, []byte("id"), "0a026964"},
35+
{"field 2", 2, []byte("x"), "120178"},
36+
}
37+
for _, c := range cases {
38+
t.Run(c.name, func(t *testing.T) {
39+
hexEqual(t, vcgrpc.EncodeBytesField(c.fieldNumber, c.value), c.expected)
40+
})
41+
}
42+
}
43+
44+
// 256-byte payload exercises the multi-byte varint length prefix
45+
// (256 -> 0x80 0x02).
46+
func TestEncodeBytesFieldLongLength(t *testing.T) {
47+
body := make([]byte, 256)
48+
for i := range body {
49+
body[i] = 'a'
50+
}
51+
got := vcgrpc.EncodeBytesField(1, body)
52+
if got[0] != 0x0a || got[1] != 0x80 || got[2] != 0x02 {
53+
t.Fatalf("expected tag 0x0a varint(256)=0x8002, got % x", got[:3])
54+
}
55+
if len(got) != 3+256 {
56+
t.Fatalf("unexpected total length %d", len(got))
57+
}
58+
}
59+
60+
func TestEncodeStringField(t *testing.T) {
61+
hexEqual(t, vcgrpc.EncodeStringField(1, "abc"), "0a03616263")
62+
}
63+
64+
// startEchoServer spins up an in-process gRPC server that echoes the
65+
// raw bytes of every unary RPC. The handler registers under
66+
// /test.Echo/* so any method name on that service hits it.
67+
func startEchoServer(t *testing.T) (string, int, func()) {
68+
t.Helper()
69+
70+
lis, err := net.Listen("tcp", "127.0.0.1:0")
71+
if err != nil {
72+
t.Fatalf("listen: %v", err)
73+
}
74+
75+
srv := grpc.NewServer(grpc.UnknownServiceHandler(func(_ any, stream grpc.ServerStream) error {
76+
var in []byte
77+
if err := stream.RecvMsg(&in); err != nil {
78+
return fmt.Errorf("RecvMsg: %w", err)
79+
}
80+
if err := stream.SendMsg(in); err != nil {
81+
return fmt.Errorf("SendMsg: %w", err)
82+
}
83+
84+
return nil
85+
}))
86+
go func() { _ = srv.Serve(lis) }()
87+
88+
addr, ok := lis.Addr().(*net.TCPAddr)
89+
if !ok {
90+
srv.Stop()
91+
t.Fatalf("listener address is not TCP: %T", lis.Addr())
92+
}
93+
host, portStr, err := net.SplitHostPort(addr.String())
94+
if err != nil {
95+
srv.Stop()
96+
t.Fatalf("split addr: %v", err)
97+
}
98+
port, err := strconv.Atoi(portStr)
99+
if err != nil {
100+
srv.Stop()
101+
t.Fatalf("parse port: %v", err)
102+
}
103+
104+
return host, port, srv.Stop
105+
}
106+
107+
func TestInvokeEchoesPayload(t *testing.T) {
108+
host, port, stop := startEchoServer(t)
109+
defer stop()
110+
111+
in := vcgrpc.EncodeStringField(1, "hello")
112+
out, ok := vcgrpc.Invoke(host, port, "/test.Echo/Echo", in, 5, false)
113+
if !ok {
114+
t.Fatal("Invoke failed")
115+
}
116+
if hex.EncodeToString(out) != hex.EncodeToString(in) {
117+
t.Fatalf("echo mismatch:\n got %x\n expected %x", out, in)
118+
}
119+
}
120+
121+
func TestInvokeRefusedConnection(t *testing.T) {
122+
// 127.0.0.1:1 is reserved and never listening.
123+
if _, ok := vcgrpc.Invoke("127.0.0.1", 1, "/test.Echo/Echo", nil, 1, false); ok {
124+
t.Fatal("Invoke should fail against an unreachable address")
125+
}
126+
}
127+
128+
func TestDialReturnsConn(t *testing.T) {
129+
host, port, stop := startEchoServer(t)
130+
defer stop()
131+
132+
conn, ok := vcgrpc.Dial(host, port, false)
133+
if !ok {
134+
t.Fatal("Dial returned ok=false against a live echo server")
135+
}
136+
defer conn.Close()
137+
138+
in := vcgrpc.EncodeStringField(1, "ping")
139+
var out []byte
140+
if err := conn.Invoke(context.Background(), "/test.Echo/Echo", in, &out); err != nil {
141+
t.Fatalf("Invoke via Dial: %v", err)
142+
}
143+
if hex.EncodeToString(out) != hex.EncodeToString(in) {
144+
t.Fatalf("dial echo mismatch:\n got %x\n expected %x", out, in)
145+
}
146+
}

0 commit comments

Comments
 (0)