From 05367d3598b43618f7e89f160f5d570f2ab1da39 Mon Sep 17 00:00:00 2001 From: neil Date: Sat, 8 Aug 2026 13:09:18 +0800 Subject: [PATCH] Listen on both IPv4 and IPv6 in standalone mode by default socat binds a single family unless told which one: up to 1.7.x the default IP version for TCP-LISTEN is 4, and 1.8.0 made it "no preference", which resolves to whatever getaddrinfo and bindv6only happen to give. So an order carrying both an IPv4 and an IPv6 identifier could never pass both http-01 challenges. Bind one socket per family instead, with ipv6only on the IPv6 one so the two do not collide. IPv4-mapped IPv6 addresses are not a portable alternative, OpenBSD does not support them at all. The IPv6 listener is best effort, a host without IPv6 still gets the IPv4 one. The python fallback does the same. --listen-v4 and --listen-v6 keep forcing a single family, and passing both now means both. Le_Listen_V4 and Le_Listen_V6 were mutually exclusive in the domain conf, which silently dropped one of them on renewal, and _starttlsserver let -4 win when both were set. Fixes #7185 --- acme.sh | 115 ++++++++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 87 insertions(+), 28 deletions(-) diff --git a/acme.sh b/acme.sh index 5b59ece4..4d4f6cbe 100755 --- a/acme.sh +++ b/acme.sh @@ -2715,6 +2715,21 @@ _clearcaconf() { _clear_conf "$CA_CONF" "$1" } +#Starts a socat listener in the background, the pid is set to _socat_pid. +#It uses the content, _content_len, _NC and _SOCAT_ERR of _startserver. +#options +_startsocat() { + _socat_opts="$1" + _debug "_NC" "$_NC $_socat_opts" + $_NC $_socat_opts SYSTEM:"sleep 1; \ +echo 'HTTP/1.0 200 OK'; \ +echo 'Content-Length\: $_content_len'; \ +echo ''; \ +printf '%s' '$content';" 2>>"$_SOCAT_ERR" & + _socat_pid="$!" + _debug "_socat_pid" "$_socat_pid" +} + # content localaddress _startserver() { content="$1" @@ -2728,16 +2743,24 @@ _startserver() { _debug Le_Listen_V4 "$Le_Listen_V4" _debug Le_Listen_V6 "$Le_Listen_V6" + _serverproc_v6="" if _exists "socat"; then _NC="socat" - if [ "$Le_Listen_V6" ]; then + SOCAT_OPTIONS6="" + if [ "$Le_Listen_V6" ] && [ -z "$Le_Listen_V4" ]; then _NC="$_NC -6" SOCAT_OPTIONS=TCP6-LISTEN - elif [ "$Le_Listen_V4" ]; then + elif [ "$Le_Listen_V4" ] && [ -z "$Le_Listen_V6" ]; then _NC="$_NC -4" SOCAT_OPTIONS=TCP4-LISTEN - else + elif [ "$ncaddr" ]; then + #a single local address belongs to a single family, let socat pick it SOCAT_OPTIONS=TCP-LISTEN + else + #listen on both ipv4 and ipv6, with one socket for each family: + #ipv4-mapped ipv6 addresses are not available everywhere. + SOCAT_OPTIONS=TCP4-LISTEN + SOCAT_OPTIONS6=TCP6-LISTEN fi if [ "$DEBUG" ] && [ "$DEBUG" -gt "1" ]; then @@ -2745,6 +2768,10 @@ _startserver() { fi SOCAT_OPTIONS=$SOCAT_OPTIONS:$Le_HTTPPort,crlf,reuseaddr,fork + if [ "$SOCAT_OPTIONS6" ]; then + #ipv6only keeps this socket from colliding with the ipv4 one + SOCAT_OPTIONS6=$SOCAT_OPTIONS6:$Le_HTTPPort,crlf,reuseaddr,fork,ipv6only=1 + fi #Adding bind to local-address if [ "$ncaddr" ]; then @@ -2753,14 +2780,14 @@ _startserver() { _content_len="$(printf "%s" "$content" | wc -c)" _debug _content_len "$_content_len" - _debug "_NC" "$_NC $SOCAT_OPTIONS" export _SOCAT_ERR="$(_mktemp)" - $_NC $SOCAT_OPTIONS SYSTEM:"sleep 1; \ -echo 'HTTP/1.0 200 OK'; \ -echo 'Content-Length\: $_content_len'; \ -echo ''; \ -printf '%s' '$content';" 2>"$_SOCAT_ERR" & - serverproc="$!" + _startsocat "$SOCAT_OPTIONS" + serverproc="$_socat_pid" + if [ "$SOCAT_OPTIONS6" ]; then + #best effort, the host may have no ipv6 support at all + _startsocat "$SOCAT_OPTIONS6" + _serverproc_v6="$_socat_pid" + fi else _PYTHON="" if _exists "python3"; then @@ -2772,21 +2799,40 @@ printf '%s' '$content';" 2>"$_SOCAT_ERR" & fi if [ "$_PYTHON" ]; then _debug "Using python: $_PYTHON" - _AF="socket.AF_INET" - _BIND_ADDR="0.0.0.0" - if [ "$Le_Listen_V6" ]; then - _AF="socket.AF_INET6" + #a comma separated list of addresses to listen on, one socket for each + _BIND_ADDR="0.0.0.0,::" + if [ "$Le_Listen_V6" ] && [ -z "$Le_Listen_V4" ]; then _BIND_ADDR="::" + elif [ "$Le_Listen_V4" ] && [ -z "$Le_Listen_V6" ]; then + _BIND_ADDR="0.0.0.0" fi if [ "$ncaddr" ]; then _BIND_ADDR="$ncaddr" fi + _debug "_BIND_ADDR" "$_BIND_ADDR" export _SOCAT_ERR="$(_mktemp)" - $_PYTHON -c "import socket,sys;s=socket.socket($_AF,socket.SOCK_STREAM);s.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1);s.bind((sys.argv[2],int(sys.argv[1])));s.listen(5);res='HTTP/1.0 200 OK\r\nContent-Length: '+str(len(sys.argv[3]))+'\r\n\r\n'+sys.argv[3]; + $_PYTHON -c "import socket,sys,select +res='HTTP/1.0 200 OK\r\nContent-Length: '+str(len(sys.argv[3]))+'\r\n\r\n'+sys.argv[3] +ads=sys.argv[2].split(',') +ls=[] +for ad in ads: + try: + sk=socket.socket(socket.AF_INET6 if ':' in ad else socket.AF_INET,socket.SOCK_STREAM) + sk.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1) + if ':' in ad and len(ads)>1: + sk.setsockopt(socket.IPPROTO_IPV6,socket.IPV6_V6ONLY,1) + sk.bind((ad,int(sys.argv[1]))) + sk.listen(5) + ls.append(sk) + except Exception: + sys.stderr.write(str(sys.exc_info()[1])+'\n') +if not ls: + sys.exit(1) while True: - c,a=s.accept() - c.sendall(res.encode() if hasattr(res, 'encode') else res) - c.close()" "$Le_HTTPPort" "$_BIND_ADDR" "$content" 2>"$_SOCAT_ERR" & + for sk in select.select(ls,[],[])[0]: + c,a=sk.accept() + c.sendall(res.encode() if hasattr(res, 'encode') else res) + c.close()" "$Le_HTTPPort" "$_BIND_ADDR" "$content" 2>"$_SOCAT_ERR" & serverproc="$!" _NC="$_PYTHON" else @@ -2809,6 +2855,11 @@ while True: _stopserver() { pid="$1" _debug "pid" "$pid" + if [ "$_serverproc_v6" ]; then + _debug "_serverproc_v6" "$_serverproc_v6" + kill $_serverproc_v6 >/dev/null 2>&1 + _serverproc_v6="" + fi if [ -z "$pid" ]; then rm -f "$_SOCAT_ERR" return @@ -2882,9 +2933,11 @@ _starttlsserver() { _debug Le_Listen_V4 "$Le_Listen_V4" _debug Le_Listen_V6 "$Le_Listen_V6" - if [ "$Le_Listen_V4" ]; then + #openssl s_server binds a single socket, so both options together can only + #mean: do not force a family, same as when neither of them is given. + if [ "$Le_Listen_V4" ] && [ -z "$Le_Listen_V6" ]; then __S_OPENSSL="$__S_OPENSSL -4" - elif [ "$Le_Listen_V6" ]; then + elif [ "$Le_Listen_V6" ] && [ -z "$Le_Listen_V4" ]; then __S_OPENSSL="$__S_OPENSSL -6" fi @@ -5986,12 +6039,17 @@ $_authorizations_map" _clearaccountconf "HTTPS_INSECURE" fi - if [ "$Le_Listen_V4" ]; then - _savedomainconf "Le_Listen_V4" "$Le_Listen_V4" - _cleardomainconf Le_Listen_V6 - elif [ "$Le_Listen_V6" ]; then - _savedomainconf "Le_Listen_V6" "$Le_Listen_V6" - _cleardomainconf Le_Listen_V4 + if [ "$Le_Listen_V4" ] || [ "$Le_Listen_V6" ]; then + if [ "$Le_Listen_V4" ]; then + _savedomainconf "Le_Listen_V4" "$Le_Listen_V4" + else + _cleardomainconf Le_Listen_V4 + fi + if [ "$Le_Listen_V6" ]; then + _savedomainconf "Le_Listen_V6" "$Le_Listen_V6" + else + _cleardomainconf Le_Listen_V6 + fi fi if [ "$Le_ForceNewDomainKey" = "1" ]; then @@ -8172,8 +8230,9 @@ Parameters: --ocsp, --ocsp-must-staple Generate OCSP-Must-Staple extension. --always-force-new-domain-key Generate new domain key on renewal. Otherwise, the domain key is not changed by default. --auto-upgrade [0|1] Valid for '--upgrade' command, indicating whether to upgrade automatically in future. Defaults to 1 if argument is omitted. - --listen-v4 Force standalone/tls server to listen at ipv4. - --listen-v6 Force standalone/tls server to listen at ipv6. + --listen-v4 Force standalone/tls server to listen at ipv4 only. + By default the standalone server listens on both ipv4 and ipv6. + --listen-v6 Force standalone/tls server to listen at ipv6 only. --request-v4 Force client requests to use ipv4 to connect to the CA server. --request-v6 Force client requests to use ipv6 to connect to the CA server. --openssl-bin Specifies a custom openssl bin location.