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

Enable IPv6 support in standalone plugin #4773

Merged
merged 27 commits into from
Jun 7, 2017
Merged

Enable IPv6 support in standalone plugin #4773

merged 27 commits into from
Jun 7, 2017

Conversation

ohemorange
Copy link
Contributor

@ohemorange ohemorange commented Jun 2, 2017

Fixes #1466.

@ohemorange ohemorange added this to the 0.15.0 milestone Jun 2, 2017
@ohemorange ohemorange requested a review from bmw June 2, 2017 00:09
@ohemorange
Copy link
Contributor Author

All of the assumptions in code here are based on things tested manually.

That is:

  • If python is compiled without IPv6, binding to an IPv6 port will raise socket.error.
  • On FreeBSD, IPv4 traffic will not forward to a socket bound on an IPv6 port, but you can ask for two different servers on the same address/port, with different protocols.
  • On Ubuntu, IPv4 traffic will forward to a socket bound on an IPv6 port, but you cannot ask for two different servers on the same address/port, with different protocols.

@ohemorange
Copy link
Contributor Author

ohemorange commented Jun 2, 2017

@bmw, I haven't added tests (nor have I tested the whole thing manually...), because I made a few design decisions here based on the discussion in #1466 and other places that might seem weirder now that they've actually been implemented. No need to do a full review just yet, but if you're good with the overall structure, I'll:

  • add unit tests
  • clean up the pieces of system test code that I've been using, and
  • find some way to get them to run automatically on different flavors of unix.

@ohemorange
Copy link
Contributor Author

ohemorange commented Jun 5, 2017

  • check there's no race condition (things are deduped later) if a response comes in on both ports before we shut the server down

@bmw
Copy link
Member

bmw commented Jun 5, 2017

For the purposes of trying to land everything we want in time for the release, I've asked @zjs to take a look at this, but one thing I wanted to mention:

On Ubuntu, IPv4 traffic will forward to a socket bound on an IPv6 port, but you cannot ask for two different servers on the same address/port, with different protocols.

If desired, I believe we can work around this and get the same behavior as the BSDs using IPV6_V6ONLY as briefly mentioned here.

zjs
zjs previously requested changes Jun 5, 2017
new_args = (new_address,) + args[1:]
server = ServerClass(*new_args, **kwargs)
except socket.error:
pass
Copy link
Collaborator

Choose a reason for hiding this comment

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

nit: It seems like some logging here would be useful for debugging things if things don't work quite as we expect.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It's expected behavior that this will fail, so setting the level to DEBUG.

# connect to all addresses
for sockname in socknames:
host, port = sockname[:2]
cert = crypto_util.probe_sni(
Copy link
Collaborator

Choose a reason for hiding this comment

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

This test fails for me locally with Error: [Errno -9] Address family for hostname not supported (which seems to match some of the Travis results.

Copy link
Collaborator

@zjs zjs Jun 5, 2017

Choose a reason for hiding this comment

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

01a5ada solves this for me and when run manually, the standalone plugin now successfully binds to IPv6 on my machine (Ubuntu, Python 2.7).

kwargs["ipv6"] = ip_version
new_address = (address[0],) + (port,) + address[2:]
new_args = (new_address,) + args[1:]
server = ServerClass(*new_args, **kwargs)
Copy link
Collaborator

Choose a reason for hiding this comment

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

nit: lint doesn't like this use of star args

Copy link
Contributor Author

Choose a reason for hiding this comment

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

ok but the linter thinks it's bad for readability but really we want it for functionality so I'm going to tell it to mind its own business.

@zjs zjs dismissed their stale review June 5, 2017 21:13

Comments have been addressed in subsequent commits.

@ohemorange
Copy link
Contributor Author

ohemorange commented Jun 6, 2017

If desired, I believe we can work around this and get the same behavior as the BSDs using IPV6_V6ONLY as briefly mentioned here.

Example of how to do this is in acme/acme/standalone_test.py, in case we want to.

@ohemorange
Copy link
Contributor Author

Anyway, I ended up mimicking FreeBSD-like conditions in the unit tests, and Travis runs things on Ubuntu, so I no longer feel as strongly that we need to automate system tests to get this in.

@ohemorange
Copy link
Contributor Author

ohemorange commented Jun 6, 2017

Manual testing I've done:

  • Ubuntu with IPv6 enabled but no DNS record so it connects over IPv4
  • Ubuntu with IPv6 enabled + DNS
  • FreeBSD with IPv6 + DNS
  • FreeBSD with IPv4 only

@ohemorange
Copy link
Contributor Author

Also, we do get BSD testing, because we run tests on OSX!

@ohemorange ohemorange assigned zjs and bmw Jun 6, 2017
@bmw
Copy link
Member

bmw commented Jun 6, 2017

I'm reviewing this.

@bmw bmw unassigned zjs Jun 6, 2017
else:
self.servers.append(server)
# Always bind to the same port
port = server.socket.getsockname()[1]
Copy link
Collaborator

Choose a reason for hiding this comment

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

The purpose of this wasn't immediately obvious to me. I think it's for when port is initially None. Perhaps for future readers it might be helpful to expand the comment to explain the case(s) where this is is used?

zjs
zjs previously approved these changes Jun 6, 2017
Copy link
Collaborator

@zjs zjs left a comment

Choose a reason for hiding this comment

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

I'm happy with this.

I'll let @bmw decide if he wants to review before merging.

Copy link
Member

@bmw bmw left a comment

Choose a reason for hiding this comment

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

Posting this a little earlier than I normally would to get all my comments out there.

Just got a single cert on both Ubuntu for two domains where one was over IPv4 and one was over IPv6. Testing the same on FreeBSD now.

Other than my nitpicky inline comments and the things we talked about out of band, one concern I have is the combination of the unreleased --<challenge-name>-address flag and this PR. What would you expect Certbot's behavior to be when using this flag and standalone? Is what we do correct/OK? Do we want to support it long term?

"""

def __init__(self, *args, **kwargs):
address = args[0]
Copy link
Member

Choose a reason for hiding this comment

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

nit: Since we'll blow up here if this class is called with no args, perhaps we should just add a server_address argument to __init__. This documents the need to provide this argument and it should have no negative effect as this is the name of the parameter used in socketserver.TCPServer (and its subclass BaseHTTPServer.HTTPServer).

def __init__(self, *args, **kwargs):
address = args[0]
port = address[1]
ServerClass = kwargs.pop("server_class", object)
Copy link
Member

@bmw bmw Jun 6, 2017

Choose a reason for hiding this comment

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

Is using object the right thing here? Other thoughts:

  1. Since this in theory is a general class for providing a single server object for IPv4 and IPv6, perhaps we should fall back to socketserver.TCPServer that all classes currently used by this server are based on.
  2. Since using this class with an ACME client without providing a server_class is almost certainly an error, we should handle it as such. This currently happens when using object, but the error is pretty cryptic. I think the best way to do this is check if server_class is in kwargs and raise a TypeError if it's not. Alternatively, we can make server_class the first argument after self to __init__ and the TypeError will be handled for you, but this further deviates the class from the server objects it's trying to wrap.

I have a preference for approach number 2 as I think it'll help catch errors. We could always go the first route later if we wanted. You can do whatever you think is best though.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It can't be socketserver.TCPServer, because that doesn't like when you pass in an ipv6 kwarg. I think it's actually fine to make it a named argument, because you probably wouldn't directly use the Base class anyway, and our TLSSNI01DualNetworkedServers and HTTP01DualNetworkedServers set that argument for you.

self.servers.append(server)
# If two servers are set up and port 0 was passed in, ensure we always
# bind to the same port for both servers.
port = server.socket.getsockname()[1]
Copy link
Member

Choose a reason for hiding this comment

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

Nice catch.

@@ -1,6 +1,8 @@
"""Tests for acme.standalone."""
import mock
Copy link
Member

Choose a reason for hiding this comment

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

super nit: Since this isn't part of the standard library, it should be moved down with the other 3rd party imports according to our style guides.

self.address_family = socket.AF_INET
socketserver.TCPServer.__init__(self, *args, **kwargs)
if ipv6:
# pylint: disable=no-member
Copy link
Member

Choose a reason for hiding this comment

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

With this line, the three disable=no-member comments below can be removed.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

pylint scoping is strange.

@ohemorange
Copy link
Contributor Author

ohemorange commented Jun 6, 2017

Other than my nitpicky inline comments and the things we talked about out of band, one concern I have is the combination of the unreleased ---address flag and this PR. What would you expect Certbot's behavior to be when using this flag and standalone? Is what we do correct/OK? Do we want to support it long term?

I built this PR on top of #4694, so I was intending it to work correctly with these changes. Manual testing indicates that's the case. It's always fine to fail to start a server. So if you pass an IPv6 address, it just won't correctly start up the IPv4 server, and on FreeBSD vice versa.

@bmw
Copy link
Member

bmw commented Jun 6, 2017

I built this PR on top of #4694, so I was intending it to work correctly with these changes. Manual testing indicates that's the case.

It works, but it's a new flag that we have a chance to modify right now. Does it work the way we want long term? To be fair, this may not be the right place for this discussion as I don't think it should block this PR, however, it may cause us to block shipping #4694.

With that said, after thinking about it, I'm personally fine with things as they are now. Given I think we should support a flag like this, I think our options are:

  1. Create both IPv4 and IPv6 versions of each flag.
  2. Have one flag that can work for either address type.

Having separate flags makes Certbot uglier and increases complexity (what happens if you provide both IPv4 and IPv6 flags?). Currently, Certbot just works whether you provide an IPv4 or IPv6 address on the command line. As other plugins support this flag, we may have to write code that determines whether an address is IPv4 or IPv6 but then every plugin can benefit from it.

The only downside I see to this approach is you can't tell Certbot to listen on both IPv4 and IPv6 and specify the interface for each one. The only reason I could come up with where someone would want to do this is they have multiple domains that each require a different interface be used but they want all domains in the same cert. This is an extreme edge case though and I don't think it's unreasonable to have the user create two certs in this case.

I'd like someone else to make sure they agree with me here, but otherwise, I think the only thing left to worry about here are my obnoxious nits.

@ohemorange
Copy link
Contributor Author

The only downside I see to this approach is you can't tell Certbot to listen on both IPv4 and IPv6 and specify the interface for each one. The only reason I could come up with where someone would want to do this is they have multiple domains that each require a different interface be used but they want all domains in the same cert. This is an extreme edge case though and I don't think it's unreasonable to have the user create two certs in this case.

I think it's reasonable to say "create two certs" for that case.

@ohemorange ohemorange assigned bmw and unassigned bmw Jun 6, 2017
Copy link
Member

@bmw bmw left a comment

Choose a reason for hiding this comment

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

LGTM

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

Successfully merging this pull request may close these issues.

None yet

3 participants