Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

support SSH connection #1014

Merged
merged 1 commit into from
Aug 14, 2018
Merged

Conversation

AkihiroSuda
Copy link
Collaborator

@AkihiroSuda AkihiroSuda commented Apr 20, 2018

Signed-off-by: Akihiro Suda [email protected]

- What I did

Added support for SSH connection. e.g. docker -H ssh://me@server

- How I did it

Followed @tonistiigi 's suggestion: #889 (comment)

The cli should accept ssh://me@server for DOCKER_HOST and -H. Using that would execute ssh with the passed config.
The ssh command would call a hidden command on the docker CLI binary on the remote side. For example, docker dial-stdio.
This command will make a connection to the local DOCKER_HOST variable (almost always the default local socket) and forward that connection on the commands stdio.
Even though this command is supposed to run locally to the dockerd binary, we think that it is an invalid configuration for this feature to remove the local docker binary so we can rely on it always being present.

- How to verify it

docker -H ssh://me@server run -it --rm busybox

- Description for the changelog

support SSH connection

- A picture of a cute animal (not mandatory but encouraged)

penguin

https://commons.wikimedia.org/wiki/File:Manchot_Ad%C3%A9lie_juv%C3%A9nile.jpg

Vendor: moby/moby#36630 (merged)


Tests (currently tested manually):

  • echo hello | ./docker -H ssh://10.2.54.48 run -i --rm busybox cat; echo world
  • ./docker -H ssh://10.2.54.48 run --rm busybox echo hello; echo world
  • ./docker -H ssh://10.2.54.48 run -i --rm busybox echo hello; echo world

@cpuguy83
Copy link
Collaborator

cpuguy83 commented Apr 23, 2018

Design LGTM! cc @tonistiigi for design as well.
Tested this out and it works pretty well.

I did notice that this:

echo hello | ./docker-dev -H ssh://docker@$ip run -i --rm busybox cat

Doesn't echo anything to my term when using the ssh connector.
But a normal interactive container works fine. Image pull also works well.

@AkihiroSuda
Copy link
Collaborator Author

@cpuguy83
Thank you for testing.

The issue can be mitigated by increasing this timeout, which is the equivalent of socat -t and nc -q: https://github.com/docker/cli/pull/1014/files#diff-065a55b1c9d14a6817c97ca733652920R17

However, increasing this value introduces extra sleep.

I'll try to find a more robust way

@AkihiroSuda AkihiroSuda reopened this Apr 23, 2018
@vdemeester
Copy link
Collaborator

Design LGTM too, this is cool 😻

Copy link
Member

@tonistiigi tonistiigi left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overall design LGTM. I don't understand the timeout logic on handling EOF though.

if !ok {
return errors.New("the raw stream connection does not implement halfCloser")
}
stdio := &cliHalfCloser{dockerCli}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It may be confusing to use dockerCli here. I'd just use os pkg.

}()
select {
case err := <-stdin2conn:
logrus.Debugf("stdin2conn: %v", err)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this wip?

// ConnectionHelper allows to connect to a remote host with custom stream provider binary.
type ConnectionHelper struct {
Dialer func(ctx context.Context, network, addr string) (net.Conn, error)
DummyHost string // dummy URL used for HTTP requests. e.g. "http://docker"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just Host. We can also leave a sane default here if WithHost is not called. So connection helper case doesn't need to call it at all (or even better if connectionhelper just returns the client options directly instead of a struct).

clientOpts = append(clientOpts, client.WithHost(host))
} else {
clientOpts = append(clientOpts, func(c *client.Client) error {
httpClient := &http.Client{
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should just add WithDialer instead of overriding client and remove WithHijackDialer. Using it would create a new client with a transport pointing to the current dialer and set it to hijack as well.

func runDialStdio(dockerCli command.Cli, opts dialStdioOptions) error {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
conn, err := dockerCli.Client().DialRaw(ctx)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this would be slightly better as a Dialer() func(ctx) (net.Conn, error) . Otherwise, it seems like a method that calls to the remote side. We could also solve this by breaking up NewClientWithOpts so that we always get access to dialer in cli before creating the client and store the dialer so we can reuse it.

}

flags := cmd.Flags()
flags.DurationVarP(&opts.timeout, "timeout", "t", 500*time.Millisecond, "After EOF from one of inputs, wait for this duration before exit")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure I understand this timeout. Why isn't the close of connection handled cleanly? This could be just a connection timeout but then it should be much bigger (20 sec) and increasing it shouldn't have any downsides when timeout isn't reached.

stdin2conn := make(chan error)
conn2stdout := make(chan error)

go func() {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These might be better handled with a wrapper around conn. But I guess I don't understand the need for the current timeout atm.

@cpuguy83
Copy link
Collaborator

cpuguy83 commented Jul 2, 2018

Any update here?

@AkihiroSuda
Copy link
Collaborator Author

Sorry still stuck at the stream connection issue #1014 (comment)

Will try to look into this again ASAP

@cpuguy83
Copy link
Collaborator

cpuguy83 commented Jul 3, 2018

No worries I just thought this was already merged and realized it wasn't!

@AkihiroSuda AkihiroSuda force-pushed the connhelper-sshonly branch 2 times, most recently from 5cd375a to e6fc199 Compare July 27, 2018 06:55
@codecov-io
Copy link

codecov-io commented Jul 27, 2018

Codecov Report

Merging #1014 into master will decrease coverage by 0.28%.
The diff coverage is 34.46%.

@@            Coverage Diff            @@
##           master   #1014      +/-   ##
=========================================
- Coverage   54.29%     54%   -0.29%     
=========================================
  Files         268     272       +4     
  Lines       17843   18068     +225     
=========================================
+ Hits         9687    9758      +71     
- Misses       7546    7694     +148     
- Partials      610     616       +6

@AkihiroSuda AkihiroSuda changed the title support SSH connection [WIP] support SSH connection Jul 27, 2018
@sandys
Copy link

sandys commented Aug 20, 2018 via email

@AkihiroSuda
Copy link
Collaborator Author

Currently no, could you open a github issue?

Secondly we have more -o options that we pass.

Any option that cannot be configured via ~/.ssh/config?

@BretFisher
Copy link
Contributor

From ops and sysadmins everywhere, we thank you for this fantastic and unexpected feature. I'm hoping this will seriously cut down the number of times I see people opening dockerd TCP w/o TLS and just opt for SSH endpoints for remote mgmt. 👍 😂

@overmike
Copy link

overmike commented Nov 9, 2018

is this possible to use in docker golang client rather than cli? just like docker-py understand the ssh:// DOCKER_HOST

@cpuguy83
Copy link
Collaborator

cpuguy83 commented Nov 9, 2018

@overmike No, none of the client helpers will set this up for you. Nothing is stopping someone from doing this themselves with the client, though. client.NewClient accepts an http client, which you can setup on your own.

The CLI is using a helper subcommand (hidden from --help) to assist in wiring up std i/o with the remote.

@overmike
Copy link

@cpuguy83 , thanks for your feedback. It clarifies I need to add connhelper code in our golang client.

I think you meant "docker system dial-stdio" for the hidden subcommand. And seems like docker-py 3.6.0 is going to support ssh DOCKER_HOST.

@bullno1
Copy link

bullno1 commented Nov 14, 2018

How does it handle authentication? Does it support signed SSH certificate? What about revocation?

@cpuguy83
Copy link
Collaborator

It just shells out to the ssh bin. You can add whatever options for the host to your ssh config. I think there is another pr to support passing ssh opts through an environment var as well.

// GetConnectionHelper returns nil without error when no helper is registered for the scheme.
// URL is like "ssh://me@server01".
func GetConnectionHelper(daemonURL string) (*ConnectionHelper, error) {
u, err := url.Parse(daemonURL)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can actually get a unparsedHost here which is not parseable - has no schema.
Like 127.0.0.1:2364

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

GetConnectionHelper returns nil without error when no helper is registered for the scheme.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think, this is actually unrelated to what I'm talking about. Just a line below you check for err from url.Parse() and since there is a possibility the passed daemonURL is in 127.0.0.1:2345 format url.Parse() will rightfully return an error per its specification - no protocol scheme. I am not sure where should the scheme be added - here or higher in the call stack but this line in particular introduced a regression in the newest stable release of docker engine - 18.09.0

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@zmilonas looks like you mean the issue that was addressed by #1443 ?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well it might have been addressed but is not fixed 😅 I will file a new issue shortly then.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's fixed on master, which is what this repository is tracking.

A backport PR is open to include it in the 18.09 codebase

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we expect it to end up in some kind of 18.09.01 release then?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, that's what the backport is for. In the meantime you could either modify your configuration (use tcp://<IP|host>:<port>), or download the static binaries for Docker 18.06 (or "nightly", which has the latest changes from "master") https://download.docker.com/linux/static/

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.