Skip to content

Migrate external EPP proxy to integrated Nomulus EPP server#3126

Open
ptkach wants to merge 2 commits into
google:masterfrom
ptkach:proxyRemoval
Open

Migrate external EPP proxy to integrated Nomulus EPP server#3126
ptkach wants to merge 2 commits into
google:masterfrom
ptkach:proxyRemoval

Conversation

@ptkach

@ptkach ptkach commented Jun 30, 2026

Copy link
Copy Markdown
Collaborator

Migrate EPP traffic handling from the external EPP proxy container to an integrated, in-process Netty-based EPP server running within the Nomulus frontend container.

  1. Integrated Netty EPP Server:
    • Replaces the standalone proxy container with an in-process Netty TCP server (EppServer) listening for registrar EPP traffic.
    • Forwards EPP requests to EPP flows via a servlet-compatible bridge (EppServiceHandler -> FakeHttpServletRequest -> RegistryServlet).
    • Offloads EPP command execution to a dedicated business thread pool (businessGroup) to prevent blocking the Netty I/O loop.
  2. Network Topology & Client IP Preservation:
    • Deploys a new epp-server GKE service using a Layer 4 regional Load Balancer with externalTrafficPolicy: Local to preserve the original client IP.
    • Extracts and propagates client IP and certificate hash through custom headers (Nomulus-Client-Address and X-SSL-Certificate) to EPP validation flows.
    • Configures a TCP socket readiness probe on the GKE EPP server deployment for health monitoring.
  3. Throttling & Rate-Limiting:
    • Implements thread-safe local connection limiting per pod for both IP and Certificate scopes using atomic map operations.
    • Enforces distributed command-level rate limiting via QuotaManager utilizing Valkey (Redis) Lua scripts.
  4. Lifecycle and Metrics Consolidation:
    • Introduces EppServerLifecycleListener to manage EPP Netty server bootstrap and teardown.
    • Consolidates the JVM-wide MetricReporter startup and shutdown under MetricsLifecycleListener to ensure JVM and EPP server metrics are correctly pushed to Stackdriver.

This change is Reviewable

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Attention Required: Lockfile Detected

This pull request contains modifications to one or more *.lockfile files. Please confirm that you have run update_dependency.sh to push new dependencies to the private repo.

Someone with Admin role must manually dismiss this review before merging.

@ptkach ptkach added the WIP Work in progress. Don't review yet. label Jun 30, 2026
Matcher matcher = CLID_PATTERN.matcher(xml);
if (matcher.find()) {
registrarId = matcher.group(1).trim();
logger.atInfo().log("Identified registrar: %s", registrarId);
Comment thread core/src/main/java/google/registry/util/FakeHttpServletRequest.java Fixed
public void channelRead(ChannelHandlerContext ctx, Object msg) {
ByteBuf buf = (ByteBuf) msg;
if (buf.equals(checkRequest)) {
ChannelFuture unusedFuture = ctx.writeAndFlush(checkResponse);
Future<?> unusedFuture = channel.close();
});
Future<?> unusedFuture = eventGroup.shutdownGracefully();
Future<?> unusedFutureBusiness = businessGroup.shutdownGracefully();
channel -> {
Future<?> unusedFuture = channel.close();
});
Future<?> unusedFuture = eventGroup.shutdownGracefully();
.values()
.forEach(
channel -> {
Future<?> unusedFuture = channel.close();
Comment thread core/src/main/java/google/registry/eppserver/EppServer.java Fixed
@jianglai

Copy link
Copy Markdown
Contributor

FWIW I think most of the new code is copied from the proxy code base. You should use git mv, which would make this PR much easier to review, not to mention to retain the history from the proxy code base.

@ptkach ptkach left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Unfortunately that wasn't possible as we want to keep old proxy running in parallel with the new one, while we're testing the new service and it might take a while, as such the old proxy deletion will be a separate change.

@ptkach made 1 comment.
Reviewable status: 0 of 42 files reviewed, 15 unresolved discussions.

@jianglai

Copy link
Copy Markdown
Contributor

That's too bad. But I think you can salvage the situation by making a copy of the current proxy code (therefore losing the history), updating your build process to build the proxy from the copy, then renaming/refactoring your existing proxy code into the epp server. There is more work, but you get a cleaner result in the end where the full history is preserved, and the refactor is easier to review as only the changes from proxy to epp server are highlighted.

I think you can ask agent to do the copying part and it should do a decent job. But it's ultimately your decision how you want to handle it.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Attention Required: Lockfile Detected

This pull request contains modifications to one or more *.lockfile files. Please confirm that you have run update_dependency.sh to push new dependencies to the private repo.

Someone with Admin role must manually dismiss this review before merging.

// Simulate FakeHttpServletResponse modifying headers and writing payload
doAnswer(
invocation -> {
FakeHttpServletRequest req = invocation.getArgument(0);

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Attention Required: Lockfile Detected

This pull request contains modifications to one or more *.lockfile files. Please confirm that you have run update_dependency.sh to push new dependencies to the private repo.

Someone with Admin role must manually dismiss this review before merging.

Comment on lines +215 to +216
@SuppressWarnings("unused")
Future<?> unusedFuture = ctx.close();
Comment on lines +210 to +211
@SuppressWarnings("unused")
Future<?> unusedFuture = ctx.writeAndFlush(out);
Comment on lines +207 to +208
@SuppressWarnings("unused")
Future<?> unusedFuture = ctx.writeAndFlush(out).addListener(ChannelFutureListener.CLOSE);
Comment on lines +172 to +173
@SuppressWarnings("unused")
Future<?> unusedFuture = ctx.close();
Comment on lines +137 to +138
@SuppressWarnings("unused")
Future<?> unusedFuture = ctx.close();
Comment on lines +129 to +130
@SuppressWarnings("unused")
Future<?> unusedFuture = ctx.close();
Comment on lines +113 to +114
@SuppressWarnings("unused")
Future<?> unusedFuture = ctx.close();
@ptkach
ptkach force-pushed the proxyRemoval branch 2 times, most recently from b104b79 to 304cf08 Compare July 16, 2026 18:56

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Attention Required: Lockfile Detected

This pull request contains modifications to one or more *.lockfile files. Please confirm that you have run update_dependency.sh to push new dependencies to the private repo.

Someone with Admin role must manually dismiss this review before merging.

ChannelHandler handler = handlerProvider.get();
String handlerName = handler.getClass().getSimpleName();
if (handlerName.equals("EppServiceHandler")) {
channelPipeline.addLast(businessGroup, handlerName, handler);

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Attention Required: Lockfile Detected

This pull request contains modifications to one or more *.lockfile files. Please confirm that you have run update_dependency.sh to push new dependencies to the private repo.

Someone with Admin role must manually dismiss this review before merging.

@ptkach ptkach removed the WIP Work in progress. Don't review yet. label Jul 16, 2026
@ptkach
ptkach requested a review from gbrodman July 16, 2026 20:45
return new QuotaResponse(((Long) result) >= 0);
} catch (Exception e) {
logger.atSevere().withCause(e).log(
"Valkey error for quota key: %s", URLEncoder.encode(key, StandardCharsets.UTF_8));

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Attention Required: Lockfile Detected

This pull request contains modifications to one or more *.lockfile files. Please confirm that you have run update_dependency.sh to push new dependencies to the private repo.

Someone with Admin role must manually dismiss this review before merging.

@gbrodman gbrodman left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

nit: should change the commit message for more details and to use an action verb

@gbrodman reviewed 44 files and all commit messages, and made 9 comments.
Reviewable status: all files reviewed, 23 unresolved discussions (waiting on ptkach).


core/src/main/java/google/registry/config/files/default-config.yaml line 437 at r6 (raw file):

eppServer:
  # The integrated EPP server listens on this port.
  port: 30002

should we default to 700? i haven't read through the rest of this PR yet so i don't know if that's a valid question


core/src/main/java/google/registry/eppserver/EppServer.java line 80 at r6 (raw file):

        ChannelHandler handler = handlerProvider.get();
        String handlerName = handler.getClass().getSimpleName();
        if (handlerName.equals("EppServiceHandler")) {

i think we can just compare handler.getClass() to the class directly, rather than hardcoding the class simple name?


core/src/main/java/google/registry/eppserver/handler/EppServiceHandler.java line 146 at r6 (raw file):

    // 2. Trigger initial EPP <greeting>
    handleEppFrame(ctx, Unpooled.wrappedBuffer(helloBytes));

i think this is happening on the main IO thread right? since this just happens after we resolve the cert. I am pretty sure we want to offload this to an executor so that the initial hello isn't blocking the IO thread


core/src/main/java/google/registry/eppserver/quota/LocalConnectionLimiter.java line 77 at r6 (raw file):

  }

  private void release(String key, ConcurrentHashMap<String, AtomicInteger> map) {

i think we need to computeIfAbsent this too otherwise one thread could increment an AtomicInteger that has already been removed from the map


core/src/main/java/google/registry/eppserver/quota/QuotaManager.java line 40 at r6 (raw file):

  /** Lua script to atomically decrement a token bucket with a TTL. */
  private static final String DECR_LUA =

you could probably use triplequoted strings for these , your call if that's cleaner


core/src/main/java/google/registry/util/FakeHttpServletRequest.java line 1 at r6 (raw file):

// Copyright 2024 The Nomulus Authors. All Rights Reserved.

2026? worth checking copyright dates on all the files since there's a ton of variance. Though I'm not sure how to treat copyright dates on the moved proxy files


core/src/main/java/google/registry/util/FakeHttpServletRequest.java line 53 at r6 (raw file):

  private final Map<String, String> headers = new HashMap<>();
  private final Map<String, Object> attributes = new HashMap<>();
  private String method = "POST";

i think many of these can be final?


jetty/deploy-nomulus-for-env.sh line 36 at r6 (raw file):

echo "Updating cluster ${parts[0]} in location ${parts[1]}..."
gcloud container fleet memberships get-credentials "${parts[0]}" --project "${project}"
for service in frontend backend pubapi console epp-server

are we just going to have this open for anyone to connect to if they want? just double checking

@gbrodman gbrodman left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

@gbrodman made 1 comment.
Reviewable status: all files reviewed, 24 unresolved discussions (waiting on ptkach).


core/src/main/java/google/registry/util/FakeHttpServletResponse.java line 68 at r6 (raw file):

  @Override
  public void addHeader(String name, String value) {
    headers.put(name, value);

not a huge deal, but i'm p sure this should append the name+value on to the existing header

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Attention Required: Lockfile Detected

This pull request contains modifications to one or more *.lockfile files. Please confirm that you have run update_dependency.sh to push new dependencies to the private repo.

Someone with Admin role must manually dismiss this review before merging.

@ptkach ptkach changed the title Proxy removal Migrate external EPP proxy to integrated Nomulus EPP server Jul 20, 2026

@ptkach ptkach left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Updated PR message and description, update commit title

@ptkach made 7 comments.
Reviewable status: 38 of 44 files reviewed, 24 unresolved discussions (waiting on gbrodman).


core/src/main/java/google/registry/config/files/default-config.yaml line 437 at r6 (raw file):

Previously, gbrodman wrote…

should we default to 700? i haven't read through the rest of this PR yet so i don't know if that's a valid question

30002 is an internal port, external port is 700, same as the old proxy


core/src/main/java/google/registry/eppserver/EppServer.java line 80 at r6 (raw file):

Previously, gbrodman wrote…

i think we can just compare handler.getClass() to the class directly, rather than hardcoding the class simple name?

Done


core/src/main/java/google/registry/eppserver/handler/EppServiceHandler.java line 146 at r6 (raw file):

Previously, gbrodman wrote…

i think this is happening on the main IO thread right? since this just happens after we resolve the cert. I am pretty sure we want to offload this to an executor so that the initial hello isn't blocking the IO thread

Good catch, I've copied it from proxy as is, but it works differently there so now executor is required.


core/src/main/java/google/registry/eppserver/quota/LocalConnectionLimiter.java line 77 at r6 (raw file):

Previously, gbrodman wrote…

i think we need to computeIfAbsent this too otherwise one thread could increment an AtomicInteger that has already been removed from the map

Good point, updated


core/src/main/java/google/registry/util/FakeHttpServletRequest.java line 53 at r6 (raw file):

Previously, gbrodman wrote…

i think many of these can be final?

Updated servername and contentType, method, requestUri, and body can not be set as final they have setters


jetty/deploy-nomulus-for-env.sh line 36 at r6 (raw file):

Previously, gbrodman wrote…

are we just going to have this open for anyone to connect to if they want? just double checking

Discussed offline

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.

4 participants