Skip to content

Commit 46c9cb6

Browse files
Merge pull request #380 from austinvazquez/vsock
Refactor: pull in firecracker-containerd's internal vsock module as a package
2 parents a6bf179 + a9057bd commit 46c9cb6

File tree

4 files changed

+338
-1
lines changed

4 files changed

+338
-1
lines changed

go.mod

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,11 @@ require (
1313
github.com/go-openapi/validate v0.20.3
1414
github.com/google/uuid v1.3.0
1515
github.com/hashicorp/go-multierror v1.1.1
16+
github.com/mdlayher/vsock v1.0.0
1617
github.com/pkg/errors v0.9.1
1718
github.com/sirupsen/logrus v1.8.1
1819
github.com/sparrc/go-ping v0.0.0-20190613174326-4e5b6552494c
1920
github.com/stretchr/testify v1.7.0
2021
github.com/vishvananda/netlink v1.1.1-0.20210330154013-f5de75959ad5
21-
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e
22+
golang.org/x/sys v0.0.0-20220204135822-1c1b9b1eba6a
2223
)

go.sum

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -453,6 +453,9 @@ github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
453453
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
454454
github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
455455
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
456+
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
457+
github.com/google/go-cmp v0.5.7 h1:81/ik6ipDQS2aGcBfIN5dHDB36BwrStyeAQquSYCV4o=
458+
github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE=
456459
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
457460
github.com/google/gofuzz v1.1.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
458461
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
@@ -558,6 +561,10 @@ github.com/mattn/go-shellwords v1.0.3/go.mod h1:3xCvwCdWdlDJUrvuMn7Wuy9eWs4pE8vq
558561
github.com/mattn/go-shellwords v1.0.12/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y=
559562
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
560563
github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4=
564+
github.com/mdlayher/socket v0.1.1 h1:q3uOGirUPfAV2MUoaC7BavjQ154J7+JOkTWyiV+intI=
565+
github.com/mdlayher/socket v0.1.1/go.mod h1:mYV5YIZAfHh4dzDVzI8x8tWLWCliuX8Mon5Awbj+qDs=
566+
github.com/mdlayher/vsock v1.0.0 h1:xHmS5JFckaaTqj6VI7QrtvQKfrAm6N5n1dcjWeNa2rM=
567+
github.com/mdlayher/vsock v1.0.0/go.mod h1:jMTGWjWntkzvtOi/ael17m1xiW22T9j2rbwsR4B/sE0=
561568
github.com/miekg/pkcs11 v1.0.3/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs=
562569
github.com/mistifyio/go-zfs v2.1.2-0.20190413222219-f784269be439+incompatible/go.mod h1:8AuVvqP/mXw1px98n46wfvcGfQ4ci2FwoAjKYxuo3Z4=
563570
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
@@ -886,6 +893,9 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v
886893
golang.org/x/net v0.0.0-20210421230115-4e50805a0758/go.mod h1:72T/g9IO56b78aLF+1Kcs5dz7/ng1VjMUvfKvpfy+jM=
887894
golang.org/x/net v0.0.0-20210428140749-89ef3d95e781 h1:DzZ89McO9/gWPsQXS/FVKAlG02ZjaQ6AlZRBimEYOd0=
888895
golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk=
896+
golang.org/x/net v0.0.0-20210928044308-7d9f5e0b762b/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
897+
golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd h1:O7DYs+zxREGLKzKoMQrtrEacpb0ZVXA5rIwylE2Xchk=
898+
golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
889899
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
890900
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
891901
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@@ -901,6 +911,8 @@ golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJ
901911
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
902912
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
903913
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
914+
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ=
915+
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
904916
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
905917
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
906918
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -968,9 +980,15 @@ golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7w
968980
golang.org/x/sys v0.0.0-20210324051608-47abb6519492/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
969981
golang.org/x/sys v0.0.0-20210420072515-93ed5bcd2bfe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
970982
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
983+
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
971984
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e h1:WUoyKPm6nCo1BnNUvPGnFG3T5DUVem42yDJZZ4CNxMA=
972985
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
986+
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
987+
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
988+
golang.org/x/sys v0.0.0-20220204135822-1c1b9b1eba6a h1:ppl5mZgokTT8uPkmYOyEUmPTr3ypaKkg5eFOGrAmxxE=
989+
golang.org/x/sys v0.0.0-20220204135822-1c1b9b1eba6a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
973990
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
991+
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
974992
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
975993
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
976994
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=

vsock/dial.go

Lines changed: 208 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,208 @@
1+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License"). You may
4+
// not use this file except in compliance with the License. A copy of the
5+
// License is located at
6+
//
7+
// http://aws.amazon.com/apache2.0/
8+
//
9+
// or in the "license" file accompanying this file. This file is distributed
10+
// on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
11+
// express or implied. See the License for the specific language governing
12+
// permissions and limitations under the License.
13+
14+
package vsock
15+
16+
import (
17+
"bufio"
18+
"context"
19+
"fmt"
20+
"net"
21+
"strings"
22+
"time"
23+
24+
"github.com/pkg/errors"
25+
"github.com/sirupsen/logrus"
26+
)
27+
28+
type Timeout struct {
29+
DialTimeout time.Duration
30+
RetryTimeout time.Duration
31+
RetryInterval time.Duration
32+
ConnectMsgTimeout time.Duration
33+
AckMsgTimeout time.Duration
34+
}
35+
36+
func DefaultTimeouts() Timeout {
37+
return Timeout{
38+
DialTimeout: 100 * time.Millisecond,
39+
RetryTimeout: 20 * time.Second,
40+
RetryInterval: 100 * time.Millisecond,
41+
ConnectMsgTimeout: 100 * time.Millisecond,
42+
AckMsgTimeout: 1 * time.Second,
43+
}
44+
}
45+
46+
// Dial connects to the Firecracker host-side vsock at the provided unix path and port.
47+
//
48+
// It will retry connect attempts if a temporary error is encountered up to a fixed
49+
// timeout or the provided request is canceled.
50+
//
51+
// udsPath specifies the file system path of the UNIX domain socket.
52+
//
53+
// port will be used in the connect message to the firecracker vsock.
54+
func Dial(ctx context.Context, logger *logrus.Entry, udsPath string, port uint32) (net.Conn, error) {
55+
return DialTimeout(ctx, logger, udsPath, port, DefaultTimeouts())
56+
}
57+
58+
// DialTimeout acts like Dial but takes a timeout.
59+
//
60+
// See func Dial for a description of the udsPath and port parameters.
61+
func DialTimeout(ctx context.Context, logger *logrus.Entry, udsPath string, port uint32, timeout Timeout) (net.Conn, error) {
62+
ticker := time.NewTicker(timeout.RetryInterval)
63+
defer ticker.Stop()
64+
65+
tickerCh := ticker.C
66+
var attemptCount int
67+
for {
68+
attemptCount++
69+
logger := logger.WithField("attempt", attemptCount)
70+
71+
select {
72+
case <-ctx.Done():
73+
return nil, ctx.Err()
74+
case <-tickerCh:
75+
conn, err := tryConnect(logger, udsPath, port, timeout)
76+
if isTemporaryNetErr(err) {
77+
err = errors.Wrap(err, "temporary vsock dial failure")
78+
logger.WithError(err).Debug()
79+
continue
80+
} else if err != nil {
81+
err = errors.Wrap(err, "non-temporary vsock dial failure")
82+
logger.WithError(err).Error()
83+
return nil, err
84+
}
85+
86+
return conn, nil
87+
}
88+
}
89+
}
90+
91+
func connectMsg(port uint32) string {
92+
// The message a host-side connection must write after connecting to a firecracker
93+
// vsock unix socket in order to establish a connection with a guest-side listener
94+
// at the provided port number. This is specified in Firecracker documentation:
95+
// https://github.com/firecracker-microvm/firecracker/blob/main/docs/vsock.md#host-initiated-connections
96+
return fmt.Sprintf("CONNECT %d\n", port)
97+
}
98+
99+
// tryConnect attempts to dial a guest vsock listener at the provided host-side
100+
// unix socket and provided guest-listener port.
101+
func tryConnect(logger *logrus.Entry, udsPath string, port uint32, timeout Timeout) (net.Conn, error) {
102+
conn, err := net.DialTimeout("unix", udsPath, timeout.DialTimeout)
103+
if err != nil {
104+
return nil, errors.Wrapf(err, "failed to dial %q within %s", udsPath, timeout.DialTimeout)
105+
}
106+
107+
defer func() {
108+
if err != nil {
109+
closeErr := conn.Close()
110+
if closeErr != nil {
111+
logger.WithError(closeErr).Error(
112+
"failed to close vsock socket after previous error")
113+
}
114+
}
115+
}()
116+
117+
msg := connectMsg(port)
118+
err = tryConnWrite(conn, msg, timeout.ConnectMsgTimeout)
119+
if err != nil {
120+
return nil, connectMsgError{
121+
cause: errors.Wrapf(err, `failed to write %q within %s`, msg, timeout.ConnectMsgTimeout),
122+
}
123+
}
124+
125+
line, err := tryConnReadUntil(conn, '\n', timeout.AckMsgTimeout)
126+
if err != nil {
127+
return nil, ackError{
128+
cause: errors.Wrapf(err, `failed to read "OK <port>" within %s`, timeout.AckMsgTimeout),
129+
}
130+
}
131+
132+
// The line would be "OK <assigned_hostside_port>\n", but we don't use the hostside port here.
133+
// https://github.com/firecracker-microvm/firecracker/blob/main/docs/vsock.md#host-initiated-connections
134+
if !strings.HasPrefix(line, "OK ") {
135+
return nil, ackError{
136+
cause: errors.Errorf(`expected to read "OK <port>", but instead read %q`, line),
137+
}
138+
}
139+
return conn, nil
140+
}
141+
142+
// tryConnReadUntil will try to do a read from the provided conn until the specified
143+
// end character is encounteed. Returning an error if the read does not complete
144+
// within the provided timeout. It will reset socket deadlines to none after returning.
145+
// It's only intended to be used for connect/ack messages, not general purpose reads
146+
// after the vsock connection is established fully.
147+
func tryConnReadUntil(conn net.Conn, end byte, timeout time.Duration) (string, error) {
148+
conn.SetDeadline(time.Now().Add(timeout))
149+
defer conn.SetDeadline(time.Time{})
150+
151+
return bufio.NewReaderSize(conn, 32).ReadString(end)
152+
}
153+
154+
// tryConnWrite will try to do a write to the provided conn, returning an error if
155+
// the write fails, is partial or does not complete within the provided timeout. It
156+
// will reset socket deadlines to none after returning. It's only intended to be
157+
// used for connect/ack messages, not general purpose writes after the vsock
158+
// connection is established fully.
159+
func tryConnWrite(conn net.Conn, expectedWrite string, timeout time.Duration) error {
160+
conn.SetDeadline(time.Now().Add(timeout))
161+
defer conn.SetDeadline(time.Time{})
162+
163+
bytesWritten, err := conn.Write([]byte(expectedWrite))
164+
if err != nil {
165+
return err
166+
}
167+
if bytesWritten != len(expectedWrite) {
168+
return errors.Errorf("incomplete write, expected %d bytes but wrote %d",
169+
len(expectedWrite), bytesWritten)
170+
}
171+
172+
return nil
173+
}
174+
175+
type connectMsgError struct {
176+
cause error
177+
}
178+
179+
func (e connectMsgError) Error() string {
180+
return errors.Wrap(e.cause, "vsock connect message failure").Error()
181+
}
182+
183+
func (e connectMsgError) Temporary() bool {
184+
return false
185+
}
186+
187+
type ackError struct {
188+
cause error
189+
}
190+
191+
func (e ackError) Error() string {
192+
return errors.Wrap(e.cause, "vsock ack message failure").Error()
193+
}
194+
195+
func (e ackError) Temporary() bool {
196+
return true
197+
}
198+
199+
// isTemporaryNetErr returns whether the provided error is a retriable
200+
// error, according to the interface defined here:
201+
// https://golang.org/pkg/net/#Error
202+
func isTemporaryNetErr(err error) bool {
203+
terr, ok := err.(interface {
204+
Temporary() bool
205+
})
206+
207+
return err != nil && ok && terr.Temporary()
208+
}

vsock/listener.go

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License"). You may
4+
// not use this file except in compliance with the License. A copy of the
5+
// License is located at
6+
//
7+
// http://aws.amazon.com/apache2.0/
8+
//
9+
// or in the "license" file accompanying this file. This file is distributed
10+
// on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
11+
// express or implied. See the License for the specific language governing
12+
// permissions and limitations under the License.
13+
14+
package vsock
15+
16+
import (
17+
"context"
18+
"net"
19+
"time"
20+
21+
"github.com/mdlayher/vsock"
22+
"github.com/pkg/errors"
23+
"github.com/sirupsen/logrus"
24+
)
25+
26+
type listener struct {
27+
listener net.Listener
28+
port uint32
29+
timeout Timeout
30+
ctx context.Context
31+
logger *logrus.Entry
32+
}
33+
34+
// Listener returns a net.Listener implementation for guest-side Firecracker
35+
// vsock connections.
36+
func Listener(ctx context.Context, logger *logrus.Entry, port uint32) (net.Listener, error) {
37+
l, err := vsock.Listen(port, nil)
38+
if err != nil {
39+
return nil, err
40+
}
41+
42+
return listener{
43+
listener: l,
44+
port: port,
45+
timeout: DefaultTimeouts(),
46+
ctx: ctx,
47+
logger: logger,
48+
}, nil
49+
}
50+
51+
func (l listener) Accept() (net.Conn, error) {
52+
ctx, cancel := context.WithTimeout(l.ctx, l.timeout.RetryTimeout)
53+
defer cancel()
54+
55+
var attemptCount int
56+
ticker := time.NewTicker(l.timeout.RetryInterval)
57+
defer ticker.Stop()
58+
tickerCh := ticker.C
59+
for {
60+
attemptCount++
61+
logger := l.logger.WithField("attempt", attemptCount)
62+
63+
select {
64+
case <-ctx.Done():
65+
return nil, ctx.Err()
66+
case <-tickerCh:
67+
conn, err := tryAccept(logger, l.listener, l.port)
68+
if isTemporaryNetErr(err) {
69+
err = errors.Wrap(err, "temporary vsock accept failure")
70+
logger.WithError(err).Debug()
71+
continue
72+
} else if err != nil {
73+
err = errors.Wrap(err, "non-temporary vsock accept failure")
74+
logger.WithError(err).Error()
75+
return nil, err
76+
}
77+
78+
return conn, nil
79+
}
80+
}
81+
}
82+
83+
func (l listener) Close() error {
84+
return l.listener.Close()
85+
}
86+
87+
func (l listener) Addr() net.Addr {
88+
return l.listener.Addr()
89+
}
90+
91+
// tryAccept attempts to accept a single host-side connection from the provided
92+
// guest-side listener at the provided port.
93+
func tryAccept(logger *logrus.Entry, listener net.Listener, port uint32) (net.Conn, error) {
94+
conn, err := listener.Accept()
95+
if err != nil {
96+
return nil, err
97+
}
98+
99+
defer func() {
100+
if err != nil {
101+
closeErr := conn.Close()
102+
if closeErr != nil {
103+
logger.WithError(closeErr).Error(
104+
"failed to close vsock after previous error")
105+
}
106+
}
107+
}()
108+
109+
return conn, nil
110+
}

0 commit comments

Comments
 (0)