[Bug] Triple HTTP/3 handshake fails on multi-homed hosts: 127.0.0.1 rewritten and QUIC responses from wrong source
- Dominant language
- Java
- Stars
- 41.6k
- Forks
- 26.4k
- Avg merge
- 15h 13m
- Merged PRs (30d)
- 4
Description
### Pre-check
- [X] I am sure that all the content I provide is in English.
- [X] I had searched in the [issues](https://github.com/apache/dubbo/issues?q=is%3Aissue) and found no similar issues.
### Apache Dubbo Component
Java SDK (apache/dubbo)
### Dubbo Version
3.3.6, OpenJDK 17, Linux x86_64 (GitHub Actions runner, ubuntu-24.04: `eth0` 10.1.0.215, `docker0` 172.18.0.1)
### Steps to reproduce this issue
Triple HTTP/3 server + client on the same machine, point-to-point, loopback target. Single maven module, deps: `org.apache.dubbo:dubbo`, `dubbo-rpc-triple`, `dubbo-remoting-http3` (all 3.3.6) + `protobuf-java` 3.25.5.
greet.proto:
```proto
syntax = "proto3";
package greet;
option java_package = "repro.greet";
option java_multiple_files = true;
message GreetRequest { string name = 1; }
message GreetResponse { string greeting = 1; }
service GreetService { rpc Greet(GreetRequest) returns (GreetResponse) {} }
```
Server: h3 enabled, negotiation off, TLS via the certs from dubbo-go-samples/http3/x509 (`dubbo.protocol.triple.http3.enabled=true`, `negotiation=false`, `dubbo.protocol.port=20000`, ssl server paths).
Client:
```java
public static void main(String[] args) throws Exception {
System.setProperty("dubbo.protocol.triple.http3.enabled", "true");
System.setProperty("dubbo.protocol.triple.http3.negotiation", "false");
System.setProperty("dubbo.ssl.client-key-cert-chain-path", "x509/server2_cert.pem");
System.setProperty("dubbo.ssl.client-private-key-path", "x509/server2_key_pkcs8.pem");
System.setProperty("dubbo.ssl.client-trust-cert-collection-path", "x509/server_ca_cert.pem");
System.out.println("filterLocalHost(127.0.0.1) -> " + NetUtils.filterLocalHost("127.0.0.1"));
ReferenceConfig referenceConfig = new ReferenceConfig<>();
referenceConfig.setInterface(GreetService.class);
referenceConfig.setUrl("tri://127.0.0.1:20000?connect.timeout=10000");
referenceConfig.setProtocol("tri");
DubboBootstrap.getInstance().reference(referenceConfig).start();
GreetService greetService = referenceConfig.get();
GreetResponse response = greetService.greet(GreetRequest.newBuilder().setName("repro").build());
System.out.println("SUCCESS! " + response.getGreeting());
}
```
On a multi-homed host the first line already prints the rewritten address, e.g. `filterLocalHost(127.0.0.1) -> 172.18.0.1`.
### Expected behavior
Client connects to 127.0.0.1:20000, handshake completes.
### Actual behavior
On a multi-homed host the connect always times out, and the target in the error is not the configured one:
```
RemotingException: client(url: tri://127.0.0.1:20000/greet.GreetService?...) failed to connect to server /172.18.0.1:20000 client-side timeout 10000ms
```
The client dials 172.18.0.1, not 127.0.0.1. tcpdump during the handshake:
```
IP 10.1.0.215.34293 > 172.18.0.1.20000: UDP, length 1200 <- client Initial
IP 10.1.0.215.20000 > 10.1.0.215.34293: UDP, length 88 <- server response, wrong source
```
The server answers from 10.1.0.215:20000 instead of 172.18.0.1:20000. The client drops handshake responses from a different address than the dialed destination and keeps retransmitting until the timeout. The same client completes the QUIC handshake against a dubbo-go triple http3 server (quic-go, bound 0.0.0.0) on the same runner.
Two code paths combine:
1. `AbstractClient.getConnectAddress()` (dubbo-remoting-api) wraps the URL host with `NetUtils.filterLocalHost(...)`. `NetUtils.isInvalidLocalHost()` treats `127.*` as invalid, so the host is replaced with `NetUtils.getLocalHost()` (172.18.0.1, the docker0 address).
2. `NettyHttp3Server` binds the QUIC transport on a wildcard `NioDatagramChannel` (`Start NettyHttp3Server bind /0.0.0.0:20000`) and responses carry the kernel-selected source (10.1.0.215, the interface towards the client) instead of the local destination address the client packet was sent to (172.18.0.1).
### Anything else
Works on single-homed hosts (macOS): rewritten address and response source coincide, handshake succeeds. Fails the same way with connect.timeout raised to 10s. Full CI run with tcpdump capture: https://github.com/apache/dubbo-go-samples/actions/runs/34701734332
### Are you willing to submit a pull request to fix on your own?
- [X] Yes I am willing to submit a pull request on my own!
Contributor guide
Research direction
Start by inspecting AbstractClient.getConnectAddress(), NetUtils.filterLocalHost(), and NetUtils.isInvalidLocalHost() in dubbo-remoting-api, then trace NettyHttp3Server's wildcard NioDatagramChannel binding. Reproduce with the provided multi-homed HTTP/3 setup and tcpdump capture. Done means the client retains the configured destination and the QUIC handshake succeeds without source-address mismatch.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- java
- Domain
- networking
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 58/100