diff mbox series

[ovs-dev,RFC,03/12] conntrack: Split the FTP and TFTP handling into separate files.

Message ID 20260408170613.587902-4-aconole@redhat.com
State New
Headers show
Series ct-offload: Introduce a conntrack offload infrastructure. | expand

Checks

Context Check Description
ovsrobot/apply-robot success apply and check: success
ovsrobot/github-robot-_Build_and_Test success github build: passed

Commit Message

Aaron Conole April 8, 2026, 5:05 p.m. UTC
The FTP and TFTP helpers were scattered all over the conntrack TU making
reading the individual FTP parts a bit difficult.  Now that the handling
is more modular, split them out into their own files.

Signed-off-by: Aaron Conole <aconole@redhat.com>
---
 lib/automake.mk         |   2 +
 lib/conntrack-ftp.c     | 689 ++++++++++++++++++++++++++++++++++++++
 lib/conntrack-private.h |  42 +++
 lib/conntrack-tftp.c    |  47 +++
 lib/conntrack.c         | 718 +---------------------------------------
 5 files changed, 786 insertions(+), 712 deletions(-)
 create mode 100644 lib/conntrack-ftp.c
 create mode 100644 lib/conntrack-tftp.c
diff mbox series

Patch

diff --git a/lib/automake.mk b/lib/automake.mk
index c6e988906f..933b71226b 100644
--- a/lib/automake.mk
+++ b/lib/automake.mk
@@ -86,9 +86,11 @@  lib_libopenvswitch_la_SOURCES = \
 	lib/compiler.h \
 	lib/connectivity.c \
 	lib/connectivity.h \
+	lib/conntrack-ftp.c \
 	lib/conntrack-icmp.c \
 	lib/conntrack-private.h \
 	lib/conntrack-tcp.c \
+	lib/conntrack-tftp.c \
 	lib/conntrack-tp.c \
 	lib/conntrack-tp.h \
 	lib/conntrack-other.c \
diff --git a/lib/conntrack-ftp.c b/lib/conntrack-ftp.c
new file mode 100644
index 0000000000..6ce17c9efe
--- /dev/null
+++ b/lib/conntrack-ftp.c
@@ -0,0 +1,689 @@ 
+/*
+ * Copyright (c) 2015-2019 Nicira, Inc.
+ * Copyright (c) 2026 Red Hat, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at:
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include <config.h>
+
+#include <ctype.h>
+#include <sys/types.h>
+#include <netinet/in.h>
+#include <string.h>
+
+#include "conntrack-private.h"
+#include "csum.h"
+#include "dp-packet.h"
+#include "openvswitch/vlog.h"
+#include "packets.h"
+#include "unaligned.h"
+#include "util.h"
+
+VLOG_DEFINE_THIS_MODULE(conntrack_ftp);
+
+/* FTP ALG mode: whether the data connection is initiated by the client
+ * (active) or the server (passive), and whether the session uses IPv6
+ * extensions (EPRT/EPSV). */
+enum ct_alg_mode {
+    CT_FTP_MODE_ACTIVE,
+    CT_FTP_MODE_PASSIVE,
+    CT_TFTP_MODE,
+};
+
+/* String buffer used for parsing FTP string messages.
+ * This is sized about twice what is needed to leave some
+ * margin of error. */
+#define LARGEST_FTP_MSG_OF_INTEREST 128
+/* FTP port string used in active mode. */
+#define FTP_PORT_CMD "PORT"
+/* FTP pasv string used in passive mode. */
+#define FTP_PASV_REPLY_CODE "227"
+/* FTP epsv string used in passive mode. */
+#define FTP_EPSV_REPLY_CODE "229"
+/* Maximum decimal digits for port in FTP command.
+ * The port is represented as two 3 digit numbers with the
+ * high part a multiple of 256. */
+#define MAX_FTP_PORT_DGTS 3
+
+/* FTP extension EPRT string used for active mode. */
+#define FTP_EPRT_CMD "EPRT"
+/* FTP extension EPSV string used for passive mode. */
+#define FTP_EPSV_REPLY "EXTENDED PASSIVE"
+/* Maximum decimal digits for port in FTP extended command. */
+#define MAX_EXT_FTP_PORT_DGTS 5
+/* FTP extended command code for IPv4. */
+#define FTP_AF_V4 '1'
+/* FTP extended command code for IPv6. */
+#define FTP_AF_V6 '2'
+
+static bool
+is_ftp_ctl(const enum ct_alg_ctl_type ct_alg_ctl)
+{
+    return ct_alg_ctl == CT_ALG_CTL_FTP;
+}
+
+static void
+replace_substring(char *substr, size_t substr_size,
+                  size_t total_size, char *rep_str,
+                  size_t rep_str_size)
+{
+    memmove(substr + rep_str_size, substr + substr_size,
+            total_size - substr_size);
+    memcpy(substr, rep_str, rep_str_size);
+}
+
+static void
+repl_bytes(char *str, char c1, char c2, int max)
+{
+    while (*str) {
+        if (*str == c1) {
+            *str = c2;
+
+            if (--max == 0) {
+                break;
+            }
+        }
+        str++;
+    }
+}
+
+/* Replaces a substring in the packet and rewrites the packet
+ * size to match.  This function assumes the caller has verified
+ * the lengths to prevent under/over flow. */
+static void
+modify_packet(struct dp_packet *pkt, char *pkt_str, size_t size,
+              char *repl_str, size_t repl_size,
+              uint32_t orig_used_size)
+{
+    replace_substring(pkt_str, size,
+                      (const char *) dp_packet_tail(pkt) - pkt_str,
+                      repl_str, repl_size);
+    dp_packet_set_size(pkt, orig_used_size + (int) repl_size - (int) size);
+}
+
+/* Replace IPV4 address in FTP message with NATed address. */
+static int
+repl_ftp_v4_addr(struct dp_packet *pkt, ovs_be32 v4_addr_rep,
+                 char *ftp_data_start,
+                 size_t addr_offset_from_ftp_data_start,
+                 size_t addr_size)
+{
+    enum { MAX_FTP_V4_NAT_DELTA = 8 };
+
+    /* EPSV mode. */
+    if (addr_offset_from_ftp_data_start == 0 &&
+        addr_size == 0) {
+        return 0;
+    }
+
+    /* Do conservative check for pathological MTU usage. */
+    uint32_t orig_used_size = dp_packet_size(pkt);
+    if (orig_used_size + MAX_FTP_V4_NAT_DELTA >
+        dp_packet_get_allocated(pkt)) {
+
+        static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(5, 5);
+        VLOG_WARN_RL(&rl, "Unsupported effective MTU %u used with FTP V4",
+                     dp_packet_get_allocated(pkt));
+        return 0;
+    }
+
+    char v4_addr_str[INET_ADDRSTRLEN] = {0};
+    ovs_assert(inet_ntop(AF_INET, &v4_addr_rep, v4_addr_str,
+                         sizeof v4_addr_str));
+    repl_bytes(v4_addr_str, '.', ',', 0);
+    modify_packet(pkt, ftp_data_start + addr_offset_from_ftp_data_start,
+                  addr_size, v4_addr_str, strlen(v4_addr_str),
+                  orig_used_size);
+    return (int) strlen(v4_addr_str) - (int) addr_size;
+}
+
+static char *
+skip_non_digits(char *str)
+{
+    while (!isdigit(*str) && *str != 0) {
+        str++;
+    }
+    return str;
+}
+
+static char *
+terminate_number_str(char *str, uint8_t max_digits)
+{
+    uint8_t digits_found = 0;
+    while (isdigit(*str) && digits_found <= max_digits) {
+        str++;
+        digits_found++;
+    }
+
+    *str = 0;
+    return str;
+}
+
+static void
+get_ftp_ctl_msg(struct dp_packet *pkt, char *ftp_msg)
+{
+    struct tcp_header *th = dp_packet_l4(pkt);
+    char *tcp_hdr = (char *) th;
+    uint32_t tcp_payload_len = dp_packet_get_tcp_payload_length(pkt);
+    size_t tcp_payload_of_interest = MIN(tcp_payload_len,
+                                         LARGEST_FTP_MSG_OF_INTEREST);
+    size_t tcp_hdr_len = TCP_OFFSET(th->tcp_ctl) * 4;
+
+    ovs_strlcpy(ftp_msg, tcp_hdr + tcp_hdr_len,
+                tcp_payload_of_interest);
+}
+
+static enum ftp_ctl_pkt
+detect_ftp_ctl_type(const struct conn_lookup_ctx *ctx,
+                    struct dp_packet *pkt)
+{
+    char ftp_msg[LARGEST_FTP_MSG_OF_INTEREST + 1] = {0};
+    get_ftp_ctl_msg(pkt, ftp_msg);
+
+    if (ctx->key.dl_type == htons(ETH_TYPE_IPV6)) {
+        if (strncasecmp(ftp_msg, FTP_EPRT_CMD, strlen(FTP_EPRT_CMD)) &&
+            !strcasestr(ftp_msg, FTP_EPSV_REPLY)) {
+            return CT_FTP_CTL_OTHER;
+        }
+    } else {
+        if (strncasecmp(ftp_msg, FTP_PORT_CMD, strlen(FTP_PORT_CMD)) &&
+            strncasecmp(ftp_msg, FTP_EPRT_CMD, strlen(FTP_EPRT_CMD)) &&
+            strncasecmp(ftp_msg, FTP_PASV_REPLY_CODE,
+                        strlen(FTP_PASV_REPLY_CODE)) &&
+            strncasecmp(ftp_msg, FTP_EPSV_REPLY_CODE,
+                        strlen(FTP_EPSV_REPLY_CODE))) {
+            return CT_FTP_CTL_OTHER;
+        }
+    }
+
+    return CT_FTP_CTL_INTEREST;
+}
+
+static enum ftp_ctl_pkt
+process_ftp_ctl_v4(struct conntrack *ct,
+                   struct dp_packet *pkt,
+                   const struct conn *conn_for_expectation,
+                   ovs_be32 *v4_addr_rep,
+                   char **ftp_data_v4_start,
+                   size_t *addr_offset_from_ftp_data_start,
+                   size_t *addr_size)
+{
+    struct tcp_header *th = dp_packet_l4(pkt);
+    size_t tcp_hdr_len = TCP_OFFSET(th->tcp_ctl) * 4;
+    char *tcp_hdr = (char *) th;
+    *ftp_data_v4_start = tcp_hdr + tcp_hdr_len;
+    char ftp_msg[LARGEST_FTP_MSG_OF_INTEREST + 1] = {0};
+    get_ftp_ctl_msg(pkt, ftp_msg);
+    char *ftp = ftp_msg;
+    struct in_addr ip_addr;
+    enum ct_alg_mode mode;
+    bool extended = false;
+
+    if (!strncasecmp(ftp, FTP_PORT_CMD, strlen(FTP_PORT_CMD))) {
+        ftp = ftp_msg + strlen(FTP_PORT_CMD);
+        mode = CT_FTP_MODE_ACTIVE;
+    } else if (!strncasecmp(ftp, FTP_EPRT_CMD, strlen(FTP_EPRT_CMD))) {
+        ftp = ftp_msg + strlen(FTP_EPRT_CMD);
+        mode = CT_FTP_MODE_ACTIVE;
+        extended = true;
+    } else if (!strncasecmp(ftp, FTP_EPSV_REPLY_CODE,
+                            strlen(FTP_EPSV_REPLY_CODE))) {
+        ftp = ftp_msg + strlen(FTP_EPSV_REPLY_CODE);
+        mode = CT_FTP_MODE_PASSIVE;
+        extended = true;
+    } else {
+        ftp = ftp_msg + strlen(FTP_PASV_REPLY_CODE);
+        mode = CT_FTP_MODE_PASSIVE;
+    }
+
+    /* Find first space. */
+    ftp = strchr(ftp, ' ');
+    if (!ftp) {
+        return CT_FTP_CTL_INVALID;
+    }
+
+    /* Find the first digit, after space. */
+    ftp = skip_non_digits(ftp);
+    if (*ftp == 0) {
+        return CT_FTP_CTL_INVALID;
+    }
+
+    /* EPRT, verify address family. */
+    if (extended && mode == CT_FTP_MODE_ACTIVE) {
+        if (ftp[0] != FTP_AF_V4 || isdigit(ftp[1])) {
+            return CT_FTP_CTL_INVALID;
+        }
+
+        ftp = skip_non_digits(ftp + 1);
+        if (*ftp == 0) {
+            return CT_FTP_CTL_INVALID;
+        }
+    }
+
+    if (!extended || mode == CT_FTP_MODE_ACTIVE) {
+        char *ip_addr_start = ftp;
+        *addr_offset_from_ftp_data_start = ip_addr_start - ftp_msg;
+        repl_bytes(ftp, ',', '.', 3);
+
+        /* Advance to end of IP address, to terminate it. */
+        while (*ftp) {
+            if (!isdigit(*ftp) && *ftp != '.') {
+                break;
+            }
+            ftp++;
+        }
+        *ftp = 0;
+        ftp++;
+
+        int rc2 = inet_pton(AF_INET, ip_addr_start, &ip_addr);
+        if (rc2 != 1) {
+            return CT_FTP_CTL_INVALID;
+        }
+
+        *addr_size = ftp - ip_addr_start - 1;
+    } else {
+        *addr_size = 0;
+        *addr_offset_from_ftp_data_start = 0;
+    }
+
+    char *save_ftp = ftp;
+    uint16_t port_hs;
+
+    if (!extended) {
+        ftp = terminate_number_str(ftp, MAX_FTP_PORT_DGTS);
+        if (!ftp) {
+            return CT_FTP_CTL_INVALID;
+        }
+        int value;
+        if (!str_to_int(save_ftp, 10, &value)) {
+            return CT_FTP_CTL_INVALID;
+        }
+
+        /* This is derived from the L4 port maximum is 65535. */
+        if (value > 255) {
+            return CT_FTP_CTL_INVALID;
+        }
+
+        port_hs = value;
+        port_hs <<= 8;
+
+        /* Skip over comma. */
+        ftp++;
+        save_ftp = ftp;
+        bool digit_found = false;
+        while (isdigit(*ftp)) {
+            ftp++;
+            digit_found = true;
+        }
+        if (!digit_found) {
+            return CT_FTP_CTL_INVALID;
+        }
+        *ftp = 0;
+        if (!str_to_int(save_ftp, 10, &value)) {
+            return CT_FTP_CTL_INVALID;
+        }
+
+        if (value > 255) {
+            return CT_FTP_CTL_INVALID;
+        }
+
+        port_hs |= value;
+    } else {
+        ftp = terminate_number_str(ftp, MAX_EXT_FTP_PORT_DGTS);
+        if (!ftp) {
+            return CT_FTP_CTL_INVALID;
+        }
+        int value;
+        if (!str_to_int(save_ftp, 10, &value)) {
+            return CT_FTP_CTL_INVALID;
+        }
+        if (value > UINT16_MAX) {
+            return CT_FTP_CTL_INVALID;
+        }
+        port_hs = (uint16_t) value;
+    }
+
+    ovs_be16 port = htons(port_hs);
+    ovs_be32 conn_ipv4_addr;
+
+    switch (mode) {
+    case CT_FTP_MODE_ACTIVE:
+        *v4_addr_rep =
+            conn_for_expectation->key_node[CT_DIR_REV].key.dst.addr.ipv4;
+        conn_ipv4_addr =
+            conn_for_expectation->key_node[CT_DIR_FWD].key.src.addr.ipv4;
+        break;
+    case CT_FTP_MODE_PASSIVE:
+        *v4_addr_rep =
+            conn_for_expectation->key_node[CT_DIR_FWD].key.dst.addr.ipv4;
+        conn_ipv4_addr =
+            conn_for_expectation->key_node[CT_DIR_REV].key.src.addr.ipv4;
+        break;
+    case CT_TFTP_MODE:
+    default:
+        OVS_NOT_REACHED();
+    }
+
+    if (!extended || mode == CT_FTP_MODE_ACTIVE) {
+        ovs_be32 ftp_ipv4_addr;
+        ftp_ipv4_addr = ip_addr.s_addr;
+        /* Although most servers will block this exploit, there may be some
+         * less well managed. */
+        if (ftp_ipv4_addr != conn_ipv4_addr && ftp_ipv4_addr != *v4_addr_rep) {
+            return CT_FTP_CTL_INVALID;
+        }
+    }
+
+    expectation_create(ct, port, conn_for_expectation,
+                       !!(pkt->md.ct_state & CS_REPLY_DIR), false, false);
+    return CT_FTP_CTL_INTEREST;
+}
+
+static char *
+skip_ipv6_digits(char *str)
+{
+    while (isxdigit(*str) || *str == ':' || *str == '.') {
+        str++;
+    }
+    return str;
+}
+
+static enum ftp_ctl_pkt
+process_ftp_ctl_v6(struct conntrack *ct,
+                   struct dp_packet *pkt,
+                   const struct conn *conn_for_exp,
+                   union ct_addr *v6_addr_rep, char **ftp_data_start,
+                   size_t *addr_offset_from_ftp_data_start,
+                   size_t *addr_size, enum ct_alg_mode *mode)
+{
+    struct tcp_header *th = dp_packet_l4(pkt);
+    size_t tcp_hdr_len = TCP_OFFSET(th->tcp_ctl) * 4;
+    char *tcp_hdr = (char *) th;
+    char ftp_msg[LARGEST_FTP_MSG_OF_INTEREST + 1] = {0};
+    get_ftp_ctl_msg(pkt, ftp_msg);
+    *ftp_data_start = tcp_hdr + tcp_hdr_len;
+    char *ftp = ftp_msg;
+    struct in6_addr ip6_addr;
+
+    if (!strncasecmp(ftp, FTP_EPRT_CMD, strlen(FTP_EPRT_CMD))) {
+        ftp = ftp_msg + strlen(FTP_EPRT_CMD);
+        ftp = skip_non_digits(ftp);
+        if (*ftp != FTP_AF_V6 || isdigit(ftp[1])) {
+            return CT_FTP_CTL_INVALID;
+        }
+        /* Jump over delimiter. */
+        ftp += 2;
+
+        memset(&ip6_addr, 0, sizeof ip6_addr);
+        char *ip_addr_start = ftp;
+        *addr_offset_from_ftp_data_start = ip_addr_start - ftp_msg;
+        ftp = skip_ipv6_digits(ftp);
+        *ftp = 0;
+        *addr_size = ftp - ip_addr_start;
+        int rc2 = inet_pton(AF_INET6, ip_addr_start, &ip6_addr);
+        if (rc2 != 1) {
+            return CT_FTP_CTL_INVALID;
+        }
+        ftp++;
+        *mode = CT_FTP_MODE_ACTIVE;
+    } else {
+        ftp = ftp_msg + strcspn(ftp_msg, "(");
+        ftp = skip_non_digits(ftp);
+        if (!isdigit(*ftp)) {
+            return CT_FTP_CTL_INVALID;
+        }
+
+        /* Not used for passive mode. */
+        *addr_offset_from_ftp_data_start = 0;
+        *addr_size = 0;
+
+        *mode = CT_FTP_MODE_PASSIVE;
+    }
+
+    char *save_ftp = ftp;
+    ftp = terminate_number_str(ftp, MAX_EXT_FTP_PORT_DGTS);
+    if (!ftp) {
+        return CT_FTP_CTL_INVALID;
+    }
+
+    int value;
+    if (!str_to_int(save_ftp, 10, &value)) {
+        return CT_FTP_CTL_INVALID;
+    }
+    if (value > CT_MAX_L4_PORT) {
+        return CT_FTP_CTL_INVALID;
+    }
+
+    uint16_t port_hs = value;
+    ovs_be16 port = htons(port_hs);
+
+    switch (*mode) {
+    case CT_FTP_MODE_ACTIVE:
+        *v6_addr_rep = conn_for_exp->key_node[CT_DIR_REV].key.dst.addr;
+        /* Although most servers will block this exploit, there may be some
+         * less well managed. */
+        if (memcmp(&ip6_addr, &v6_addr_rep->ipv6, sizeof ip6_addr) &&
+            memcmp(&ip6_addr,
+                   &conn_for_exp->key_node[CT_DIR_FWD].key.src.addr.ipv6,
+                   sizeof ip6_addr)) {
+            return CT_FTP_CTL_INVALID;
+        }
+        break;
+    case CT_FTP_MODE_PASSIVE:
+        *v6_addr_rep = conn_for_exp->key_node[CT_DIR_FWD].key.dst.addr;
+        break;
+    case CT_TFTP_MODE:
+    default:
+        OVS_NOT_REACHED();
+    }
+
+    expectation_create(ct, port, conn_for_exp,
+                       !!(pkt->md.ct_state & CS_REPLY_DIR), false, false);
+    return CT_FTP_CTL_INTEREST;
+}
+
+static int
+repl_ftp_v6_addr(struct dp_packet *pkt, union ct_addr v6_addr_rep,
+                 char *ftp_data_start,
+                 size_t addr_offset_from_ftp_data_start,
+                 size_t addr_size, enum ct_alg_mode mode)
+{
+    /* This is slightly bigger than really possible. */
+    enum { MAX_FTP_V6_NAT_DELTA = 45 };
+
+    if (mode == CT_FTP_MODE_PASSIVE) {
+        return 0;
+    }
+
+    /* Do conservative check for pathological MTU usage. */
+    uint32_t orig_used_size = dp_packet_size(pkt);
+    if (orig_used_size + MAX_FTP_V6_NAT_DELTA >
+        dp_packet_get_allocated(pkt)) {
+
+        static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(5, 5);
+        VLOG_WARN_RL(&rl, "Unsupported effective MTU %u used with FTP V6",
+                     dp_packet_get_allocated(pkt));
+        return 0;
+    }
+
+    char v6_addr_str[INET6_ADDRSTRLEN] = {0};
+    ovs_assert(inet_ntop(AF_INET6, &v6_addr_rep.ipv6, v6_addr_str,
+                         sizeof v6_addr_str));
+    modify_packet(pkt, ftp_data_start + addr_offset_from_ftp_data_start,
+                  addr_size, v6_addr_str, strlen(v6_addr_str),
+                  orig_used_size);
+    return (int) strlen(v6_addr_str) - (int) addr_size;
+}
+
+/* Increment/decrement a TCP sequence number. */
+static void
+adj_seqnum(ovs_16aligned_be32 *val, int32_t inc)
+{
+    put_16aligned_be32(val, htonl(ntohl(get_16aligned_be32(val)) + inc));
+}
+
+static void
+handle_ftp_ctl(struct conntrack *ct, const struct conn_lookup_ctx *ctx,
+               struct dp_packet *pkt, struct conn *ec, long long now,
+               enum ftp_ctl_pkt ftp_ctl, bool nat)
+{
+    struct ip_header *l3_hdr = dp_packet_l3(pkt);
+    ovs_be32 v4_addr_rep = 0;
+    union ct_addr v6_addr_rep;
+    size_t addr_offset_from_ftp_data_start = 0;
+    size_t addr_size = 0;
+    char *ftp_data_start;
+    enum ct_alg_mode mode = CT_FTP_MODE_ACTIVE;
+
+    if (detect_ftp_ctl_type(ctx, pkt) != ftp_ctl) {
+        return;
+    }
+
+    struct ovs_16aligned_ip6_hdr *nh6 = dp_packet_l3(pkt);
+    int64_t seq_skew = 0;
+
+    if (ftp_ctl == CT_FTP_CTL_INTEREST) {
+        enum ftp_ctl_pkt rc;
+        if (ctx->key.dl_type == htons(ETH_TYPE_IPV6)) {
+            rc = process_ftp_ctl_v6(ct, pkt, ec,
+                                    &v6_addr_rep, &ftp_data_start,
+                                    &addr_offset_from_ftp_data_start,
+                                    &addr_size, &mode);
+        } else {
+            rc = process_ftp_ctl_v4(ct, pkt, ec,
+                                    &v4_addr_rep, &ftp_data_start,
+                                    &addr_offset_from_ftp_data_start,
+                                    &addr_size);
+        }
+        if (rc == CT_FTP_CTL_INVALID) {
+            static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(5, 5);
+            VLOG_WARN_RL(&rl, "Invalid FTP control packet format");
+            pkt->md.ct_state |= CS_TRACKED | CS_INVALID;
+            return;
+        } else if (rc == CT_FTP_CTL_INTEREST) {
+            uint16_t ip_len;
+
+            if (ctx->key.dl_type == htons(ETH_TYPE_IPV6)) {
+                if (nat) {
+                    seq_skew = repl_ftp_v6_addr(pkt, v6_addr_rep,
+                                   ftp_data_start,
+                                   addr_offset_from_ftp_data_start,
+                                   addr_size, mode);
+                }
+
+                if (seq_skew) {
+                    ip_len = ntohs(nh6->ip6_ctlun.ip6_un1.ip6_un1_plen) +
+                        seq_skew;
+                    nh6->ip6_ctlun.ip6_un1.ip6_un1_plen = htons(ip_len);
+                }
+            } else {
+                if (nat) {
+                    seq_skew = repl_ftp_v4_addr(pkt, v4_addr_rep,
+                                   ftp_data_start,
+                                   addr_offset_from_ftp_data_start,
+                                   addr_size);
+                }
+                if (seq_skew) {
+                    ip_len = ntohs(l3_hdr->ip_tot_len) + seq_skew;
+                    if (dp_packet_ip_checksum_valid(pkt)) {
+                        dp_packet_ip_checksum_set_partial(pkt);
+                    } else {
+                        l3_hdr->ip_csum = recalc_csum16(l3_hdr->ip_csum,
+                                                        l3_hdr->ip_tot_len,
+                                                        htons(ip_len));
+                    }
+                    l3_hdr->ip_tot_len = htons(ip_len);
+                }
+            }
+        } else {
+            OVS_NOT_REACHED();
+        }
+    }
+
+    struct tcp_header *th = dp_packet_l4(pkt);
+
+    if (nat && ec->seq_skew != 0) {
+        ctx->reply != ec->seq_skew_dir ?
+            adj_seqnum(&th->tcp_ack, -ec->seq_skew) :
+            adj_seqnum(&th->tcp_seq, ec->seq_skew);
+    }
+
+    if (dp_packet_l4_checksum_valid(pkt)) {
+        dp_packet_l4_checksum_set_partial(pkt);
+    } else {
+        th->tcp_csum = 0;
+        if (ctx->key.dl_type == htons(ETH_TYPE_IPV6)) {
+            th->tcp_csum = packet_csum_upperlayer6(nh6, th, ctx->key.nw_proto,
+                               dp_packet_l4_size(pkt));
+        } else {
+            uint32_t tcp_csum = packet_csum_pseudoheader(l3_hdr);
+            th->tcp_csum = csum_finish(
+                 csum_continue(tcp_csum, th, dp_packet_l4_size(pkt)));
+        }
+    }
+
+    if (seq_skew) {
+        conn_seq_skew_set(ct, ec, now, seq_skew + ec->seq_skew,
+                          ctx->reply);
+    }
+}
+
+/* FTP requires sequence-number tracking to stay in sync with the source of
+ * any sequence skew introduced by address/port rewriting.  This hook
+ * interleaves handle_ftp_ctl() calls with conn_update_state() depending on
+ * packet direction so that the skew accounting is always correct. */
+static bool
+ftp_conn_update_state_hook(struct conntrack *ct, struct dp_packet *pkt,
+                           struct conn_lookup_ctx *ctx, struct conn *conn,
+                           const struct nat_action_info_t *nat_action_info,
+                           enum ct_alg_ctl_type ct_alg_ctl, long long now,
+                           bool *create_new_conn)
+{
+    if (!is_ftp_ctl(ct_alg_ctl)) {
+        return false;
+    }
+
+    /* Keep sequence tracking in sync with the source of the sequence skew. */
+    ovs_mutex_lock(&conn->lock);
+    if (ctx->reply != conn->seq_skew_dir) {
+        handle_ftp_ctl(ct, ctx, pkt, conn, now, CT_FTP_CTL_OTHER,
+                       !!nat_action_info);
+        /* conn_update_state acquires conn->lock for unrelated fields. */
+        ovs_mutex_unlock(&conn->lock);
+        *create_new_conn = conn_update_state(ct, pkt, ctx, conn, now);
+    } else {
+        ovs_mutex_unlock(&conn->lock);
+        *create_new_conn = conn_update_state(ct, pkt, ctx, conn, now);
+        ovs_mutex_lock(&conn->lock);
+        if (!*create_new_conn) {
+            handle_ftp_ctl(ct, ctx, pkt, conn, now, CT_FTP_CTL_OTHER,
+                           !!nat_action_info);
+        }
+        ovs_mutex_unlock(&conn->lock);
+    }
+    return true;
+}
+
+void
+conntrack_ftp_init(void)
+{
+    static struct ovsthread_once once = OVSTHREAD_ONCE_INITIALIZER;
+
+    if (ovsthread_once_start(&once)) {
+        conn_update_state_hook_register(CT_HOOK_PRI_NORMAL,
+                                        ftp_conn_update_state_hook);
+        alg_helpers[CT_ALG_CTL_FTP] = handle_ftp_ctl;
+        ovsthread_once_done(&once);
+    }
+}
diff --git a/lib/conntrack-private.h b/lib/conntrack-private.h
index a5bf1bb519..8eab1d3703 100644
--- a/lib/conntrack-private.h
+++ b/lib/conntrack-private.h
@@ -177,6 +177,9 @@  enum ct_ephemeral_range {
     MAX_NAT_EPHEMERAL_PORT = 65535
 };
 
+/* The maximum TCP or UDP port number. */
+#define CT_MAX_L4_PORT 65535
+
 #define IN_RANGE(curr, min, max) \
     (curr >= min && curr <= max)
 
@@ -261,6 +264,9 @@  enum ct_alg_ctl_type {
     /* SIP is not enabled through OpenFlow and is present only as an example
      * of an ALG that allows a wildcard source IP address. */
     CT_ALG_CTL_SIP,
+
+    /* MAX ALG */
+    CT_ALG_CTL_MAX,
 };
 
 extern struct ct_l4_proto ct_proto_tcp;
@@ -289,6 +295,28 @@  struct conn_lookup_ctx {
     bool icmp_related;
 };
 
+/* FTP control-packet classification used by ALG helpers.
+ * CT_FTP_CTL_INTEREST carries an address/port specifier (PORT, PASV, EPRT,
+ * EPSV); CT_FTP_CTL_OTHER does not; CT_FTP_CTL_INVALID is malformed. */
+enum ftp_ctl_pkt {
+    CT_FTP_CTL_INTEREST,
+    CT_FTP_CTL_OTHER,
+    CT_FTP_CTL_INVALID,
+};
+
+/* ALG helper callback signature.  Each registered helper receives the
+ * classified control-packet type so it can decide whether to act. */
+typedef void (*alg_helper)(struct conntrack *ct,
+                           const struct conn_lookup_ctx *ctx,
+                           struct dp_packet *pkt,
+                           struct conn *conn_for_expectation,
+                           long long now, enum ftp_ctl_pkt ftp_ctl,
+                           bool nat);
+
+/* Array indexed by ct_alg_ctl_type; populated by per-module init functions
+ * (conntrack_ftp_init, conntrack_tftp_init, ...) before first use. */
+extern alg_helper alg_helpers[];
+
 /* conn_update_state_dist() hook
  *
  * Modules may register a hook to intercept connection state transitions.
@@ -323,6 +351,20 @@  void conn_update_state_hook_register(int priority,
                                      conn_update_state_hook_fn);
 void conn_update_state_hook_unregister(conn_update_state_hook_fn);
 
+/* Functions in conntrack.c that ALG modules need. */
+bool conn_update_state(struct conntrack *ct, struct dp_packet *pkt,
+                       struct conn_lookup_ctx *ctx, struct conn *conn,
+                       long long now);
+void conn_seq_skew_set(struct conntrack *ct, const struct conn *conn_in,
+                       long long now, int seq_skew, bool seq_skew_dir);
+void expectation_create(struct conntrack *ct, ovs_be16 dst_port,
+                        const struct conn *parent_conn, bool reply,
+                        bool src_ip_wc, bool skip_nat);
+
+/* ALG module initialization functions. */
+void conntrack_ftp_init(void);
+void conntrack_tftp_init(void);
+
 /* conn_private_get() / conn_private_set()
  *
  * Fast-path accessors for per-connection private storage slots.  Both
diff --git a/lib/conntrack-tftp.c b/lib/conntrack-tftp.c
new file mode 100644
index 0000000000..61297f7240
--- /dev/null
+++ b/lib/conntrack-tftp.c
@@ -0,0 +1,47 @@ 
+/*
+ * Copyright (c) 2015-2019 Nicira, Inc.
+ * Copyright (c) 2026 Red Hat, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at:
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include <config.h>
+
+#include "conntrack-private.h"
+#include "dp-packet.h"
+#include "ovs-thread.h"
+#include "packets.h"
+
+static void
+handle_tftp_ctl(struct conntrack *ct,
+                const struct conn_lookup_ctx *ctx OVS_UNUSED,
+                struct dp_packet *pkt, struct conn *conn_for_expectation,
+                long long now OVS_UNUSED, enum ftp_ctl_pkt ftp_ctl OVS_UNUSED,
+                bool nat OVS_UNUSED)
+{
+    expectation_create(ct,
+                       conn_for_expectation->key_node[CT_DIR_FWD].key.src.port,
+                       conn_for_expectation,
+                       !!(pkt->md.ct_state & CS_REPLY_DIR), false, false);
+}
+
+void
+conntrack_tftp_init(void)
+{
+    static struct ovsthread_once once = OVSTHREAD_ONCE_INITIALIZER;
+
+    if (ovsthread_once_start(&once)) {
+        alg_helpers[CT_ALG_CTL_TFTP] = handle_tftp_ctl;
+        ovsthread_once_done(&once);
+    }
+}
diff --git a/lib/conntrack.c b/lib/conntrack.c
index d81abe456a..462c0e0ad1 100644
--- a/lib/conntrack.c
+++ b/lib/conntrack.c
@@ -55,20 +55,6 @@  COVERAGE_DEFINE(conntrack_l4csum_err);
 COVERAGE_DEFINE(conntrack_lookup_natted_miss);
 COVERAGE_DEFINE(conntrack_zone_full);
 
-enum ftp_ctl_pkt {
-    /* Control packets with address and/or port specifiers. */
-    CT_FTP_CTL_INTEREST,
-    /* Control packets without address and/or port specifiers. */
-    CT_FTP_CTL_OTHER,
-    CT_FTP_CTL_INVALID,
-};
-
-enum ct_alg_mode {
-    CT_FTP_MODE_ACTIVE,
-    CT_FTP_MODE_PASSIVE,
-    CT_TFTP_MODE,
-};
-
 struct zone_limit {
     struct cmap_node node;
     struct conntrack_zone_limit czl;
@@ -117,24 +103,6 @@  static struct alg_exp_node *
 expectation_lookup(struct hmap *alg_expectations, const struct conn_key *key,
                    uint32_t basis, bool src_ip_wc);
 
-static int
-repl_ftp_v4_addr(struct dp_packet *pkt, ovs_be32 v4_addr_rep,
-                 char *ftp_data_v4_start,
-                 size_t addr_offset_from_ftp_data_start, size_t addr_size);
-
-static enum ftp_ctl_pkt
-process_ftp_ctl_v4(struct conntrack *ct,
-                   struct dp_packet *pkt,
-                   const struct conn *conn_for_expectation,
-                   ovs_be32 *v4_addr_rep,
-                   char **ftp_data_v4_start,
-                   size_t *addr_offset_from_ftp_data_start,
-                   size_t *addr_size);
-
-static enum ftp_ctl_pkt
-detect_ftp_ctl_type(const struct conn_lookup_ctx *ctx,
-                    struct dp_packet *pkt);
-
 static void
 expectation_clean(struct conntrack *ct, const struct conn_key *parent_key);
 
@@ -170,64 +138,8 @@  struct ct_update_hook {
 static struct ct_update_hook ct_update_hooks[CT_UPDATE_STATE_HOOKS_MAX];
 static size_t n_ct_update_hooks;
 
-static bool ftp_conn_update_state_hook(struct conntrack *, struct dp_packet *,
-                                       struct conn_lookup_ctx *, struct conn *,
-                                       const struct nat_action_info_t *,
-                                       enum ct_alg_ctl_type, long long,
-                                       bool *);
-
-static void
-handle_ftp_ctl(struct conntrack *ct, const struct conn_lookup_ctx *ctx,
-               struct dp_packet *pkt, struct conn *ec, long long now,
-               enum ftp_ctl_pkt ftp_ctl, bool nat);
-
-static void
-handle_tftp_ctl(struct conntrack *ct,
-                const struct conn_lookup_ctx *ctx OVS_UNUSED,
-                struct dp_packet *pkt, struct conn *conn_for_expectation,
-                long long now OVS_UNUSED, enum ftp_ctl_pkt ftp_ctl OVS_UNUSED,
-                bool nat OVS_UNUSED);
-
-typedef void (*alg_helper)(struct conntrack *ct,
-                           const struct conn_lookup_ctx *ctx,
-                           struct dp_packet *pkt,
-                           struct conn *conn_for_expectation,
-                           long long now, enum ftp_ctl_pkt ftp_ctl,
-                           bool nat);
-
-static alg_helper alg_helpers[] = {
-    [CT_ALG_CTL_NONE] = NULL,
-    [CT_ALG_CTL_FTP] = handle_ftp_ctl,
-    [CT_ALG_CTL_TFTP] = handle_tftp_ctl,
-};
+alg_helper alg_helpers[CT_ALG_CTL_MAX];
 
-/* The maximum TCP or UDP port number. */
-#define CT_MAX_L4_PORT 65535
-/* String buffer used for parsing FTP string messages.
- * This is sized about twice what is needed to leave some
- * margin of error. */
-#define LARGEST_FTP_MSG_OF_INTEREST 128
-/* FTP port string used in active mode. */
-#define FTP_PORT_CMD "PORT"
-/* FTP pasv string used in passive mode. */
-#define FTP_PASV_REPLY_CODE "227"
-/* FTP epsv string used in passive mode. */
-#define FTP_EPSV_REPLY_CODE "229"
-/* Maximum decimal digits for port in FTP command.
- * The port is represented as two 3 digit numbers with the
- * high part a multiple of 256. */
-#define MAX_FTP_PORT_DGTS 3
-
-/* FTP extension EPRT string used for active mode. */
-#define FTP_EPRT_CMD "EPRT"
-/* FTP extension EPSV string used for passive mode. */
-#define FTP_EPSV_REPLY "EXTENDED PASSIVE"
-/* Maximum decimal digits for port in FTP extended command. */
-#define MAX_EXT_FTP_PORT_DGTS 5
-/* FTP extended command code for IPv4. */
-#define FTP_AF_V4 '1'
-/* FTP extended command code for IPv6. */
-#define FTP_AF_V6 '2'
 /* Used to indicate a wildcard L4 source port number for ALGs.
  * This is used for port numbers that we cannot predict in
  * expectations. */
@@ -311,8 +223,8 @@  conntrack_init(void)
         l4_protos[IPPROTO_ICMP] = &ct_proto_icmp4;
         l4_protos[IPPROTO_ICMPV6] = &ct_proto_icmp6;
 
-        conn_update_state_hook_register(CT_HOOK_PRI_NORMAL,
-                                        ftp_conn_update_state_hook);
+        conntrack_ftp_init();
+        conntrack_tftp_init();
 
         ovsthread_once_done(&setup_l4_once);
     }
@@ -835,12 +747,6 @@  get_ip_proto(const struct dp_packet *pkt)
     return ip_proto;
 }
 
-static bool
-is_ftp_ctl(const enum ct_alg_ctl_type ct_alg_ctl)
-{
-    return ct_alg_ctl == CT_ALG_CTL_FTP;
-}
-
 static enum ct_alg_ctl_type
 get_alg_ctl_type(const struct dp_packet *pkt, const char *helper)
 {
@@ -1044,7 +950,7 @@  nat_packet(struct dp_packet *pkt, struct conn *conn, bool reply, bool related)
     }
 }
 
-static void
+void
 conn_seq_skew_set(struct conntrack *ct, const struct conn *conn_in,
                   long long now, int seq_skew, bool seq_skew_dir)
 {
@@ -1202,7 +1108,7 @@  nat_res_exhaustion:
     return NULL;
 }
 
-static bool
+bool
 conn_update_state(struct conntrack *ct, struct dp_packet *pkt,
                   struct conn_lookup_ctx *ctx, struct conn *conn,
                   long long now)
@@ -1322,38 +1228,6 @@  check_orig_tuple(struct conntrack *ct, struct dp_packet *pkt,
     return *conn ? true : false;
 }
 
-static bool
-ftp_conn_update_state_hook(struct conntrack *ct, struct dp_packet *pkt,
-                           struct conn_lookup_ctx *ctx, struct conn *conn,
-                           const struct nat_action_info_t *nat_action_info,
-                           enum ct_alg_ctl_type ct_alg_ctl, long long now,
-                           bool *create_new_conn)
-{
-    if (!is_ftp_ctl(ct_alg_ctl)) {
-        return false;
-    }
-
-    /* Keep sequence tracking in sync with the source of the sequence skew. */
-    ovs_mutex_lock(&conn->lock);
-    if (ctx->reply != conn->seq_skew_dir) {
-        handle_ftp_ctl(ct, ctx, pkt, conn, now, CT_FTP_CTL_OTHER,
-                       !!nat_action_info);
-        /* conn_update_state acquires conn->lock for unrelated fields. */
-        ovs_mutex_unlock(&conn->lock);
-        *create_new_conn = conn_update_state(ct, pkt, ctx, conn, now);
-    } else {
-        ovs_mutex_unlock(&conn->lock);
-        *create_new_conn = conn_update_state(ct, pkt, ctx, conn, now);
-        ovs_mutex_lock(&conn->lock);
-        if (!*create_new_conn) {
-            handle_ftp_ctl(ct, ctx, pkt, conn, now, CT_FTP_CTL_OTHER,
-                           !!nat_action_info);
-        }
-        ovs_mutex_unlock(&conn->lock);
-    }
-    return true;
-}
-
 /* Distribute a connection state-transition event to registered hooks.
  * Returns true if a hook handled the update (and set *create_new_conn),
  * false if the caller should fall through to default conn_update_state(). */
@@ -3238,7 +3112,7 @@  expectation_clean(struct conntrack *ct, const struct conn_key *parent_key)
     ovs_rwlock_unlock(&ct->resources_lock);
 }
 
-static void
+void
 expectation_create(struct conntrack *ct, ovs_be16 dst_port,
                    const struct conn *parent_conn, bool reply, bool src_ip_wc,
                    bool skip_nat)
@@ -3312,467 +3186,6 @@  expectation_create(struct conntrack *ct, ovs_be16 dst_port,
     ovs_rwlock_unlock(&ct->resources_lock);
 }
 
-static void
-replace_substring(char *substr, size_t substr_size,
-                  size_t total_size, char *rep_str,
-                  size_t rep_str_size)
-{
-    memmove(substr + rep_str_size, substr + substr_size,
-            total_size - substr_size);
-    memcpy(substr, rep_str, rep_str_size);
-}
-
-static void
-repl_bytes(char *str, char c1, char c2, int max)
-{
-    while (*str) {
-        if (*str == c1) {
-            *str = c2;
-
-            if (--max == 0) {
-                break;
-            }
-        }
-        str++;
-    }
-}
-
-/* Replaces a substring in the packet and rewrites the packet
- * size to match.  This function assumes the caller has verified
- * the lengths to prevent under/over flow. */
-static void
-modify_packet(struct dp_packet *pkt, char *pkt_str, size_t size,
-              char *repl_str, size_t repl_size,
-              uint32_t orig_used_size)
-{
-    replace_substring(pkt_str, size,
-                      (const char *) dp_packet_tail(pkt) - pkt_str,
-                      repl_str, repl_size);
-    dp_packet_set_size(pkt, orig_used_size + (int) repl_size - (int) size);
-}
-
-/* Replace IPV4 address in FTP message with NATed address. */
-static int
-repl_ftp_v4_addr(struct dp_packet *pkt, ovs_be32 v4_addr_rep,
-                 char *ftp_data_start,
-                 size_t addr_offset_from_ftp_data_start,
-                 size_t addr_size)
-{
-    enum { MAX_FTP_V4_NAT_DELTA = 8 };
-
-    /* EPSV mode. */
-    if (addr_offset_from_ftp_data_start == 0 &&
-        addr_size == 0) {
-        return 0;
-    }
-
-    /* Do conservative check for pathological MTU usage. */
-    uint32_t orig_used_size = dp_packet_size(pkt);
-    if (orig_used_size + MAX_FTP_V4_NAT_DELTA >
-        dp_packet_get_allocated(pkt)) {
-
-        static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(5, 5);
-        VLOG_WARN_RL(&rl, "Unsupported effective MTU %u used with FTP V4",
-                     dp_packet_get_allocated(pkt));
-        return 0;
-    }
-
-    char v4_addr_str[INET_ADDRSTRLEN] = {0};
-    ovs_assert(inet_ntop(AF_INET, &v4_addr_rep, v4_addr_str,
-                         sizeof v4_addr_str));
-    repl_bytes(v4_addr_str, '.', ',', 0);
-    modify_packet(pkt, ftp_data_start + addr_offset_from_ftp_data_start,
-                  addr_size, v4_addr_str, strlen(v4_addr_str),
-                  orig_used_size);
-    return (int) strlen(v4_addr_str) - (int) addr_size;
-}
-
-static char *
-skip_non_digits(char *str)
-{
-    while (!isdigit(*str) && *str != 0) {
-        str++;
-    }
-    return str;
-}
-
-static char *
-terminate_number_str(char *str, uint8_t max_digits)
-{
-    uint8_t digits_found = 0;
-    while (isdigit(*str) && digits_found <= max_digits) {
-        str++;
-        digits_found++;
-    }
-
-    *str = 0;
-    return str;
-}
-
-
-static void
-get_ftp_ctl_msg(struct dp_packet *pkt, char *ftp_msg)
-{
-    struct tcp_header *th = dp_packet_l4(pkt);
-    char *tcp_hdr = (char *) th;
-    uint32_t tcp_payload_len = dp_packet_get_tcp_payload_length(pkt);
-    size_t tcp_payload_of_interest = MIN(tcp_payload_len,
-                                         LARGEST_FTP_MSG_OF_INTEREST);
-    size_t tcp_hdr_len = TCP_OFFSET(th->tcp_ctl) * 4;
-
-    ovs_strlcpy(ftp_msg, tcp_hdr + tcp_hdr_len,
-                tcp_payload_of_interest);
-}
-
-static enum ftp_ctl_pkt
-detect_ftp_ctl_type(const struct conn_lookup_ctx *ctx,
-                    struct dp_packet *pkt)
-{
-    char ftp_msg[LARGEST_FTP_MSG_OF_INTEREST + 1] = {0};
-    get_ftp_ctl_msg(pkt, ftp_msg);
-
-    if (ctx->key.dl_type == htons(ETH_TYPE_IPV6)) {
-        if (strncasecmp(ftp_msg, FTP_EPRT_CMD, strlen(FTP_EPRT_CMD)) &&
-            !strcasestr(ftp_msg, FTP_EPSV_REPLY)) {
-            return CT_FTP_CTL_OTHER;
-        }
-    } else {
-        if (strncasecmp(ftp_msg, FTP_PORT_CMD, strlen(FTP_PORT_CMD)) &&
-            strncasecmp(ftp_msg, FTP_EPRT_CMD, strlen(FTP_EPRT_CMD)) &&
-            strncasecmp(ftp_msg, FTP_PASV_REPLY_CODE,
-                        strlen(FTP_PASV_REPLY_CODE)) &&
-            strncasecmp(ftp_msg, FTP_EPSV_REPLY_CODE,
-                        strlen(FTP_EPSV_REPLY_CODE))) {
-            return CT_FTP_CTL_OTHER;
-        }
-    }
-
-    return CT_FTP_CTL_INTEREST;
-}
-
-static enum ftp_ctl_pkt
-process_ftp_ctl_v4(struct conntrack *ct,
-                   struct dp_packet *pkt,
-                   const struct conn *conn_for_expectation,
-                   ovs_be32 *v4_addr_rep,
-                   char **ftp_data_v4_start,
-                   size_t *addr_offset_from_ftp_data_start,
-                   size_t *addr_size)
-{
-    struct tcp_header *th = dp_packet_l4(pkt);
-    size_t tcp_hdr_len = TCP_OFFSET(th->tcp_ctl) * 4;
-    char *tcp_hdr = (char *) th;
-    *ftp_data_v4_start = tcp_hdr + tcp_hdr_len;
-    char ftp_msg[LARGEST_FTP_MSG_OF_INTEREST + 1] = {0};
-    get_ftp_ctl_msg(pkt, ftp_msg);
-    char *ftp = ftp_msg;
-    struct in_addr ip_addr;
-    enum ct_alg_mode mode;
-    bool extended = false;
-
-    if (!strncasecmp(ftp, FTP_PORT_CMD, strlen(FTP_PORT_CMD))) {
-        ftp = ftp_msg + strlen(FTP_PORT_CMD);
-        mode = CT_FTP_MODE_ACTIVE;
-    } else if (!strncasecmp(ftp, FTP_EPRT_CMD, strlen(FTP_EPRT_CMD))) {
-        ftp = ftp_msg + strlen(FTP_EPRT_CMD);
-        mode = CT_FTP_MODE_ACTIVE;
-        extended = true;
-    } else if (!strncasecmp(ftp, FTP_EPSV_REPLY_CODE,
-                            strlen(FTP_EPSV_REPLY_CODE))) {
-        ftp = ftp_msg + strlen(FTP_EPSV_REPLY_CODE);
-        mode = CT_FTP_MODE_PASSIVE;
-        extended = true;
-    } else {
-        ftp = ftp_msg + strlen(FTP_PASV_REPLY_CODE);
-        mode = CT_FTP_MODE_PASSIVE;
-    }
-
-    /* Find first space. */
-    ftp = strchr(ftp, ' ');
-    if (!ftp) {
-        return CT_FTP_CTL_INVALID;
-    }
-
-    /* Find the first digit, after space. */
-    ftp = skip_non_digits(ftp);
-    if (*ftp == 0) {
-        return CT_FTP_CTL_INVALID;
-    }
-
-    /* EPRT, verify address family. */
-    if (extended && mode == CT_FTP_MODE_ACTIVE) {
-        if (ftp[0] != FTP_AF_V4 || isdigit(ftp[1])) {
-            return CT_FTP_CTL_INVALID;
-        }
-
-        ftp = skip_non_digits(ftp + 1);
-        if (*ftp == 0) {
-            return CT_FTP_CTL_INVALID;
-        }
-    }
-
-    if (!extended || mode == CT_FTP_MODE_ACTIVE) {
-        char *ip_addr_start = ftp;
-        *addr_offset_from_ftp_data_start = ip_addr_start - ftp_msg;
-        repl_bytes(ftp, ',', '.', 3);
-
-        /* Advance to end of IP address, to terminate it. */
-        while (*ftp) {
-            if (!isdigit(*ftp) && *ftp != '.') {
-                break;
-            }
-            ftp++;
-        }
-        *ftp = 0;
-        ftp++;
-
-        int rc2 = inet_pton(AF_INET, ip_addr_start, &ip_addr);
-        if (rc2 != 1) {
-            return CT_FTP_CTL_INVALID;
-        }
-
-        *addr_size = ftp - ip_addr_start - 1;
-    } else {
-        *addr_size = 0;
-        *addr_offset_from_ftp_data_start = 0;
-    }
-
-    char *save_ftp = ftp;
-    uint16_t port_hs;
-
-    if (!extended) {
-        ftp = terminate_number_str(ftp, MAX_FTP_PORT_DGTS);
-        if (!ftp) {
-            return CT_FTP_CTL_INVALID;
-        }
-        int value;
-        if (!str_to_int(save_ftp, 10, &value)) {
-            return CT_FTP_CTL_INVALID;
-        }
-
-        /* This is derived from the L4 port maximum is 65535. */
-        if (value > 255) {
-            return CT_FTP_CTL_INVALID;
-        }
-
-        port_hs = value;
-        port_hs <<= 8;
-
-        /* Skip over comma. */
-        ftp++;
-        save_ftp = ftp;
-        bool digit_found = false;
-        while (isdigit(*ftp)) {
-            ftp++;
-            digit_found = true;
-        }
-        if (!digit_found) {
-            return CT_FTP_CTL_INVALID;
-        }
-        *ftp = 0;
-        if (!str_to_int(save_ftp, 10, &value)) {
-            return CT_FTP_CTL_INVALID;
-        }
-
-        if (value > 255) {
-            return CT_FTP_CTL_INVALID;
-        }
-
-        port_hs |= value;
-    } else {
-        ftp = terminate_number_str(ftp, MAX_EXT_FTP_PORT_DGTS);
-        if (!ftp) {
-            return CT_FTP_CTL_INVALID;
-        }
-        int value;
-        if (!str_to_int(save_ftp, 10, &value)) {
-            return CT_FTP_CTL_INVALID;
-        }
-        if (value > UINT16_MAX) {
-            return CT_FTP_CTL_INVALID;
-        }
-        port_hs = (uint16_t) value;
-    }
-
-    ovs_be16 port = htons(port_hs);
-    ovs_be32 conn_ipv4_addr;
-
-    switch (mode) {
-    case CT_FTP_MODE_ACTIVE:
-        *v4_addr_rep =
-            conn_for_expectation->key_node[CT_DIR_REV].key.dst.addr.ipv4;
-        conn_ipv4_addr =
-            conn_for_expectation->key_node[CT_DIR_FWD].key.src.addr.ipv4;
-        break;
-    case CT_FTP_MODE_PASSIVE:
-        *v4_addr_rep =
-            conn_for_expectation->key_node[CT_DIR_FWD].key.dst.addr.ipv4;
-        conn_ipv4_addr =
-            conn_for_expectation->key_node[CT_DIR_REV].key.src.addr.ipv4;
-        break;
-    case CT_TFTP_MODE:
-    default:
-        OVS_NOT_REACHED();
-    }
-
-    if (!extended || mode == CT_FTP_MODE_ACTIVE) {
-        ovs_be32 ftp_ipv4_addr;
-        ftp_ipv4_addr = ip_addr.s_addr;
-        /* Although most servers will block this exploit, there may be some
-         * less well managed. */
-        if (ftp_ipv4_addr != conn_ipv4_addr && ftp_ipv4_addr != *v4_addr_rep) {
-            return CT_FTP_CTL_INVALID;
-        }
-    }
-
-    expectation_create(ct, port, conn_for_expectation,
-                       !!(pkt->md.ct_state & CS_REPLY_DIR), false, false);
-    return CT_FTP_CTL_INTEREST;
-}
-
-static char *
-skip_ipv6_digits(char *str)
-{
-    while (isxdigit(*str) || *str == ':' || *str == '.') {
-        str++;
-    }
-    return str;
-}
-
-static enum ftp_ctl_pkt
-process_ftp_ctl_v6(struct conntrack *ct,
-                   struct dp_packet *pkt,
-                   const struct conn *conn_for_exp,
-                   union ct_addr *v6_addr_rep, char **ftp_data_start,
-                   size_t *addr_offset_from_ftp_data_start,
-                   size_t *addr_size, enum ct_alg_mode *mode)
-{
-    struct tcp_header *th = dp_packet_l4(pkt);
-    size_t tcp_hdr_len = TCP_OFFSET(th->tcp_ctl) * 4;
-    char *tcp_hdr = (char *) th;
-    char ftp_msg[LARGEST_FTP_MSG_OF_INTEREST + 1] = {0};
-    get_ftp_ctl_msg(pkt, ftp_msg);
-    *ftp_data_start = tcp_hdr + tcp_hdr_len;
-    char *ftp = ftp_msg;
-    struct in6_addr ip6_addr;
-
-    if (!strncasecmp(ftp, FTP_EPRT_CMD, strlen(FTP_EPRT_CMD))) {
-        ftp = ftp_msg + strlen(FTP_EPRT_CMD);
-        ftp = skip_non_digits(ftp);
-        if (*ftp != FTP_AF_V6 || isdigit(ftp[1])) {
-            return CT_FTP_CTL_INVALID;
-        }
-        /* Jump over delimiter. */
-        ftp += 2;
-
-        memset(&ip6_addr, 0, sizeof ip6_addr);
-        char *ip_addr_start = ftp;
-        *addr_offset_from_ftp_data_start = ip_addr_start - ftp_msg;
-        ftp = skip_ipv6_digits(ftp);
-        *ftp = 0;
-        *addr_size = ftp - ip_addr_start;
-        int rc2 = inet_pton(AF_INET6, ip_addr_start, &ip6_addr);
-        if (rc2 != 1) {
-            return CT_FTP_CTL_INVALID;
-        }
-        ftp++;
-        *mode = CT_FTP_MODE_ACTIVE;
-    } else {
-        ftp = ftp_msg + strcspn(ftp_msg, "(");
-        ftp = skip_non_digits(ftp);
-        if (!isdigit(*ftp)) {
-            return CT_FTP_CTL_INVALID;
-        }
-
-        /* Not used for passive mode. */
-        *addr_offset_from_ftp_data_start = 0;
-        *addr_size = 0;
-
-        *mode = CT_FTP_MODE_PASSIVE;
-    }
-
-    char *save_ftp = ftp;
-    ftp = terminate_number_str(ftp, MAX_EXT_FTP_PORT_DGTS);
-    if (!ftp) {
-        return CT_FTP_CTL_INVALID;
-    }
-
-    int value;
-    if (!str_to_int(save_ftp, 10, &value)) {
-        return CT_FTP_CTL_INVALID;
-    }
-    if (value > CT_MAX_L4_PORT) {
-        return CT_FTP_CTL_INVALID;
-    }
-
-    uint16_t port_hs = value;
-    ovs_be16 port = htons(port_hs);
-
-    switch (*mode) {
-    case CT_FTP_MODE_ACTIVE:
-        *v6_addr_rep = conn_for_exp->key_node[CT_DIR_REV].key.dst.addr;
-        /* Although most servers will block this exploit, there may be some
-         * less well managed. */
-        if (memcmp(&ip6_addr, &v6_addr_rep->ipv6, sizeof ip6_addr) &&
-            memcmp(&ip6_addr,
-                   &conn_for_exp->key_node[CT_DIR_FWD].key.src.addr.ipv6,
-                   sizeof ip6_addr)) {
-            return CT_FTP_CTL_INVALID;
-        }
-        break;
-    case CT_FTP_MODE_PASSIVE:
-        *v6_addr_rep = conn_for_exp->key_node[CT_DIR_FWD].key.dst.addr;
-        break;
-    case CT_TFTP_MODE:
-    default:
-        OVS_NOT_REACHED();
-    }
-
-    expectation_create(ct, port, conn_for_exp,
-                       !!(pkt->md.ct_state & CS_REPLY_DIR), false, false);
-    return CT_FTP_CTL_INTEREST;
-}
-
-static int
-repl_ftp_v6_addr(struct dp_packet *pkt, union ct_addr v6_addr_rep,
-                 char *ftp_data_start,
-                 size_t addr_offset_from_ftp_data_start,
-                 size_t addr_size, enum ct_alg_mode mode)
-{
-    /* This is slightly bigger than really possible. */
-    enum { MAX_FTP_V6_NAT_DELTA = 45 };
-
-    if (mode == CT_FTP_MODE_PASSIVE) {
-        return 0;
-    }
-
-    /* Do conservative check for pathological MTU usage. */
-    uint32_t orig_used_size = dp_packet_size(pkt);
-    if (orig_used_size + MAX_FTP_V6_NAT_DELTA >
-        dp_packet_get_allocated(pkt)) {
-
-        static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(5, 5);
-        VLOG_WARN_RL(&rl, "Unsupported effective MTU %u used with FTP V6",
-                     dp_packet_get_allocated(pkt));
-        return 0;
-    }
-
-    char v6_addr_str[INET6_ADDRSTRLEN] = {0};
-    ovs_assert(inet_ntop(AF_INET6, &v6_addr_rep.ipv6, v6_addr_str,
-                         sizeof v6_addr_str));
-    modify_packet(pkt, ftp_data_start + addr_offset_from_ftp_data_start,
-                  addr_size, v6_addr_str, strlen(v6_addr_str),
-                  orig_used_size);
-    return (int) strlen(v6_addr_str) - (int) addr_size;
-}
-
-/* Increment/decrement a TCP sequence number. */
-static void
-adj_seqnum(ovs_16aligned_be32 *val, int32_t inc)
-{
-    put_16aligned_be32(val, htonl(ntohl(get_16aligned_be32(val)) + inc));
-}
-
 void
 conn_update_state_hook_register(int priority, conn_update_state_hook_fn fn)
 {
@@ -3801,122 +3214,3 @@  conn_update_state_hook_unregister(conn_update_state_hook_fn fn)
         }
     }
 }
-
-static void
-handle_ftp_ctl(struct conntrack *ct, const struct conn_lookup_ctx *ctx,
-               struct dp_packet *pkt, struct conn *ec, long long now,
-               enum ftp_ctl_pkt ftp_ctl, bool nat)
-{
-    struct ip_header *l3_hdr = dp_packet_l3(pkt);
-    ovs_be32 v4_addr_rep = 0;
-    union ct_addr v6_addr_rep;
-    size_t addr_offset_from_ftp_data_start = 0;
-    size_t addr_size = 0;
-    char *ftp_data_start;
-    enum ct_alg_mode mode = CT_FTP_MODE_ACTIVE;
-
-    if (detect_ftp_ctl_type(ctx, pkt) != ftp_ctl) {
-        return;
-    }
-
-    struct ovs_16aligned_ip6_hdr *nh6 = dp_packet_l3(pkt);
-    int64_t seq_skew = 0;
-
-    if (ftp_ctl == CT_FTP_CTL_INTEREST) {
-        enum ftp_ctl_pkt rc;
-        if (ctx->key.dl_type == htons(ETH_TYPE_IPV6)) {
-            rc = process_ftp_ctl_v6(ct, pkt, ec,
-                                    &v6_addr_rep, &ftp_data_start,
-                                    &addr_offset_from_ftp_data_start,
-                                    &addr_size, &mode);
-        } else {
-            rc = process_ftp_ctl_v4(ct, pkt, ec,
-                                    &v4_addr_rep, &ftp_data_start,
-                                    &addr_offset_from_ftp_data_start,
-                                    &addr_size);
-        }
-        if (rc == CT_FTP_CTL_INVALID) {
-            static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(5, 5);
-            VLOG_WARN_RL(&rl, "Invalid FTP control packet format");
-            pkt->md.ct_state |= CS_TRACKED | CS_INVALID;
-            return;
-        } else if (rc == CT_FTP_CTL_INTEREST) {
-            uint16_t ip_len;
-
-            if (ctx->key.dl_type == htons(ETH_TYPE_IPV6)) {
-                if (nat) {
-                    seq_skew = repl_ftp_v6_addr(pkt, v6_addr_rep,
-                                   ftp_data_start,
-                                   addr_offset_from_ftp_data_start,
-                                   addr_size, mode);
-                }
-
-                if (seq_skew) {
-                    ip_len = ntohs(nh6->ip6_ctlun.ip6_un1.ip6_un1_plen) +
-                        seq_skew;
-                    nh6->ip6_ctlun.ip6_un1.ip6_un1_plen = htons(ip_len);
-                }
-            } else {
-                if (nat) {
-                    seq_skew = repl_ftp_v4_addr(pkt, v4_addr_rep,
-                                   ftp_data_start,
-                                   addr_offset_from_ftp_data_start,
-                                   addr_size);
-                }
-                if (seq_skew) {
-                    ip_len = ntohs(l3_hdr->ip_tot_len) + seq_skew;
-                    if (dp_packet_ip_checksum_valid(pkt)) {
-                        dp_packet_ip_checksum_set_partial(pkt);
-                    } else {
-                        l3_hdr->ip_csum = recalc_csum16(l3_hdr->ip_csum,
-                                                        l3_hdr->ip_tot_len,
-                                                        htons(ip_len));
-                    }
-                    l3_hdr->ip_tot_len = htons(ip_len);
-                }
-            }
-        } else {
-            OVS_NOT_REACHED();
-        }
-    }
-
-    struct tcp_header *th = dp_packet_l4(pkt);
-
-    if (nat && ec->seq_skew != 0) {
-        ctx->reply != ec->seq_skew_dir ?
-            adj_seqnum(&th->tcp_ack, -ec->seq_skew) :
-            adj_seqnum(&th->tcp_seq, ec->seq_skew);
-    }
-
-    if (dp_packet_l4_checksum_valid(pkt)) {
-        dp_packet_l4_checksum_set_partial(pkt);
-    } else {
-        th->tcp_csum = 0;
-        if (ctx->key.dl_type == htons(ETH_TYPE_IPV6)) {
-            th->tcp_csum = packet_csum_upperlayer6(nh6, th, ctx->key.nw_proto,
-                               dp_packet_l4_size(pkt));
-        } else {
-            uint32_t tcp_csum = packet_csum_pseudoheader(l3_hdr);
-            th->tcp_csum = csum_finish(
-                 csum_continue(tcp_csum, th, dp_packet_l4_size(pkt)));
-        }
-    }
-
-    if (seq_skew) {
-        conn_seq_skew_set(ct, ec, now, seq_skew + ec->seq_skew,
-                          ctx->reply);
-    }
-}
-
-static void
-handle_tftp_ctl(struct conntrack *ct,
-                const struct conn_lookup_ctx *ctx OVS_UNUSED,
-                struct dp_packet *pkt, struct conn *conn_for_expectation,
-                long long now OVS_UNUSED, enum ftp_ctl_pkt ftp_ctl OVS_UNUSED,
-                bool nat OVS_UNUSED)
-{
-    expectation_create(ct,
-                       conn_for_expectation->key_node[CT_DIR_FWD].key.src.port,
-                       conn_for_expectation,
-                       !!(pkt->md.ct_state & CS_REPLY_DIR), false, false);
-}