@@ -36,6 +36,8 @@ TEST_PROGS := \
test-chachapoly.sh \
test-close-socket-tcp.sh \
test-close-socket.sh \
+ test-epoch-tcp.sh \
+ test-epoch.sh \
test-float.sh \
test-large-mtu.sh \
test-mark.sh \
@@ -12,6 +12,7 @@ OVPN_TCP_PEERS_FILE=${OVPN_TCP_PEERS_FILE:-tcp_peers.txt}
OVPN_CLI=${OVPN_CLI:-${OVPN_COMMON_DIR}/ovpn-cli}
OVPN_YNL=${OVPN_YNL:-${OVPN_COMMON_DIR}/../../../../net/ynl/pyynl/cli.py}
OVPN_ALG=${OVPN_ALG:-aes}
+OVPN_KEY_TYPE=${OVPN_KEY_TYPE:-direct}
OVPN_PROTO=${OVPN_PROTO:-UDP}
OVPN_FLOAT=${OVPN_FLOAT:-0}
OVPN_SYMMETRIC_ID=${OVPN_SYMMETRIC_ID:-0}
@@ -184,14 +185,26 @@ ovpn_build_capture_filter() {
# address. The IPv6 branch assumes there are no extension
# headers in the outer packet.
if [[ "${2}" == *:* ]]; then
- printf "ip6 and ip6[6] = 17 and ip6[48:4] = %s" "${1}"
+ printf "ip6 and ip6[6] = 17 and ip6[48:4] = %s" \
+ "${1}"
+ if [ "${OVPN_KEY_TYPE}" == "epoch" ]; then
+ printf " and ip6[52:2] = 0x0001"
+ fi
else
printf "ip and udp[8:4] = %s" "${1}"
+ if [ "${OVPN_KEY_TYPE}" == "epoch" ]; then
+ printf " and udp[12:2] = 0x0001"
+ fi
fi
else
# openvpn over TCP prepends a 2-byte packet length ahead of the
# DATA_V2 opcode, so skip it before matching the payload header
- printf "ip and tcp[(((tcp[12] & 0xf0) >> 2) + 2):4] = %s" "${1}"
+ printf "ip and tcp[(((tcp[12] & 0xf0) >> 2) + 2):4] = %s" \
+ "${1}"
+ if [ "${OVPN_KEY_TYPE}" == "epoch" ]; then
+ printf " and tcp[(((tcp[12] & 0xf0) >> 2) + 6):2] = "
+ printf "0x0001"
+ fi
fi
}
@@ -222,8 +235,8 @@ ovpn_add_peer() {
for p in $(seq 1 ${OVPN_NUM_PEERS}); do
ip netns exec "${server_ns}" ${OVPN_CLI} \
- new_key tun0 ${p} 1 0 ${OVPN_ALG} 0 \
- data64.key
+ new_key tun0 ${p} 1 0 ${OVPN_ALG} \
+ ${OVPN_KEY_TYPE} 0 data64.key
done
else
peer_ns="ovpn_peer${1}"
@@ -245,7 +258,8 @@ ovpn_add_peer() {
tun${1} ${PEER_ID} ${TX_ID} ${LPORT} ${RADDR} \
${RPORT}
ip netns exec "${peer_ns}" ${OVPN_CLI} new_key tun${1} \
- ${PEER_ID} 1 0 ${OVPN_ALG} 1 data64.key
+ ${PEER_ID} 1 0 ${OVPN_ALG} ${OVPN_KEY_TYPE} \
+ 1 data64.key
fi
else
if [ ${1} -eq 0 ]; then
@@ -254,7 +268,8 @@ ovpn_add_peer() {
for p in $(seq 1 ${OVPN_NUM_PEERS}); do
ip netns exec "${server_ns}" \
${OVPN_CLI} new_key tun0 ${p} \
- 1 0 ${OVPN_ALG} 0 data64.key
+ 1 0 ${OVPN_ALG} \
+ ${OVPN_KEY_TYPE} 0 data64.key
done
}) &
sleep 5
@@ -269,7 +284,8 @@ ovpn_add_peer() {
TX_ID=${1}
fi
ip netns exec "${peer_ns}" ${OVPN_CLI} connect tun${1} \
- ${PEER_ID} ${TX_ID} 10.10.${1}.1 1 data64.key
+ ${PEER_ID} ${TX_ID} 10.10.${1}.1 1 \
+ ${OVPN_KEY_TYPE} data64.key
fi
fi
}
@@ -61,6 +61,11 @@ enum ovpn_key_direction {
KEY_DIR_OUT,
};
+enum ovpn_key_type {
+ KEY_TYPE_DIRECT = 0,
+ KEY_TYPE_EPOCH,
+};
+
#define KEY_LEN (256 / 8)
#define NONCE_LEN 8
@@ -130,6 +135,7 @@ struct ovpn_ctx {
__u32 keepalive_interval;
__u32 keepalive_timeout;
+ enum ovpn_key_type key_type;
enum ovpn_key_direction key_dir;
enum ovpn_key_slot key_slot;
int key_id;
@@ -451,6 +457,18 @@ static int ovpn_parse_cipher(const char *cipher, struct ovpn_ctx *ctx)
return 0;
}
+static int ovpn_parse_key_type(const char *type, struct ovpn_ctx *ctx)
+{
+ if (strcmp(type, "direct") == 0)
+ ctx->key_type = KEY_TYPE_DIRECT;
+ else if (strcmp(type, "epoch") == 0)
+ ctx->key_type = KEY_TYPE_EPOCH;
+ else
+ return -ENOTSUP;
+
+ return 0;
+}
+
static int ovpn_parse_key_direction(const char *dir, struct ovpn_ctx *ctx)
{
int in_dir;
@@ -921,9 +939,38 @@ static int ovpn_get_peer(struct ovpn_ctx *ovpn)
return ret;
}
+static int ovpn_put_direct_key(struct nl_msg *msg, int attr, const __u8 *key,
+ const __u8 *nonce)
+{
+ struct nlattr *key_dir;
+
+ key_dir = nla_nest_start(msg, attr);
+ NLA_PUT(msg, OVPN_A_KEYDIR_CIPHER_KEY, KEY_LEN, key);
+ NLA_PUT(msg, OVPN_A_KEYDIR_NONCE_TAIL, NONCE_LEN, nonce);
+ nla_nest_end(msg, key_dir);
+
+ return 0;
+nla_put_failure:
+ return -1;
+}
+
+static int ovpn_put_epoch_key(struct nl_msg *msg, int attr, const __u8 *key)
+{
+ struct nlattr *epoch;
+
+ epoch = nla_nest_start(msg, attr);
+ NLA_PUT(msg, OVPN_A_EPOCH_KEY, KEY_LEN, key);
+ NLA_PUT_U32(msg, OVPN_A_EPOCH_CIPHER_KEY_LEN, KEY_LEN);
+ nla_nest_end(msg, epoch);
+
+ return 0;
+nla_put_failure:
+ return -1;
+}
+
static int ovpn_new_key(struct ovpn_ctx *ovpn)
{
- struct nlattr *keyconf, *key_dir;
+ struct nlattr *keyconf;
struct nl_ctx *ctx;
int ret = -1;
@@ -937,15 +984,37 @@ static int ovpn_new_key(struct ovpn_ctx *ovpn)
NLA_PUT_U32(ctx->nl_msg, OVPN_A_KEYCONF_KEY_ID, ovpn->key_id);
NLA_PUT_U32(ctx->nl_msg, OVPN_A_KEYCONF_CIPHER_ALG, ovpn->cipher);
- key_dir = nla_nest_start(ctx->nl_msg, OVPN_A_KEYCONF_ENCRYPT_DIR);
- NLA_PUT(ctx->nl_msg, OVPN_A_KEYDIR_CIPHER_KEY, KEY_LEN, ovpn->key_enc);
- NLA_PUT(ctx->nl_msg, OVPN_A_KEYDIR_NONCE_TAIL, NONCE_LEN, ovpn->nonce);
- nla_nest_end(ctx->nl_msg, key_dir);
+ switch (ovpn->key_type) {
+ case KEY_TYPE_DIRECT:
+ ret = ovpn_put_direct_key(ctx->nl_msg,
+ OVPN_A_KEYCONF_ENCRYPT_DIR,
+ ovpn->key_enc, ovpn->nonce);
+ if (ret)
+ goto nla_put_failure;
- key_dir = nla_nest_start(ctx->nl_msg, OVPN_A_KEYCONF_DECRYPT_DIR);
- NLA_PUT(ctx->nl_msg, OVPN_A_KEYDIR_CIPHER_KEY, KEY_LEN, ovpn->key_dec);
- NLA_PUT(ctx->nl_msg, OVPN_A_KEYDIR_NONCE_TAIL, NONCE_LEN, ovpn->nonce);
- nla_nest_end(ctx->nl_msg, key_dir);
+ ret = ovpn_put_direct_key(ctx->nl_msg,
+ OVPN_A_KEYCONF_DECRYPT_DIR,
+ ovpn->key_dec, ovpn->nonce);
+ if (ret)
+ goto nla_put_failure;
+ break;
+ case KEY_TYPE_EPOCH:
+ ret = ovpn_put_epoch_key(ctx->nl_msg,
+ OVPN_A_KEYCONF_ENCRYPT_EPOCH,
+ ovpn->key_enc);
+ if (ret)
+ goto nla_put_failure;
+
+ ret = ovpn_put_epoch_key(ctx->nl_msg,
+ OVPN_A_KEYCONF_DECRYPT_EPOCH,
+ ovpn->key_dec);
+ if (ret)
+ goto nla_put_failure;
+ break;
+ default:
+ ret = -EINVAL;
+ goto nla_put_failure;
+ }
nla_nest_end(ctx->nl_msg, keyconf);
@@ -1691,7 +1760,7 @@ static void usage(const char *cmd)
"\tipv6: whether the socket should listen to the IPv6 wildcard address\n");
fprintf(stderr,
- "* connect <iface> <peer_id> <tx_id> <raddr> <rport> [key_file]: start connecting peer of TCP-based VPN session\n");
+ "* connect <iface> <peer_id> <tx_id> <raddr> <rport> [key_type key_file]: start connecting peer of TCP-based VPN session\n");
fprintf(stderr, "\tiface: ovpn interface name\n");
fprintf(stderr,
"\tpeer_id: peer ID found in data packets received from this peer\n");
@@ -1699,8 +1768,8 @@ static void usage(const char *cmd)
"\ttx_id: peer ID to be used when sending to this peer, 'none' for symmetric peer ID\n");
fprintf(stderr, "\traddr: peer IP address to connect to\n");
fprintf(stderr, "\trport: peer TCP port to connect to\n");
- fprintf(stderr,
- "\tkey_file: file containing the symmetric key for encryption\n");
+ fprintf(stderr, "\tkey_type: key type, supported: direct, epoch\n");
+ fprintf(stderr, "\tkey_file: file containing the pre-shared key\n");
fprintf(stderr,
"* new_peer <iface> <peer_id> <tx_id> <lport> <raddr> <rport> [vpnaddr]: add new peer\n");
@@ -1748,7 +1817,7 @@ static void usage(const char *cmd)
"\tpeer_id: peer ID of the peer to query. All peers are returned if omitted\n");
fprintf(stderr,
- "* new_key <iface> <peer_id> <slot> <key_id> <cipher> <key_dir> <key_file>: set data channel key\n");
+ "* new_key <iface> <peer_id> <slot> <key_id> <cipher> <key_type> <key_dir> <key_file>: set data channel key\n");
fprintf(stderr, "\tiface: ovpn interface name\n");
fprintf(stderr,
"\tpeer_id: peer ID of the peer to configure the key for\n");
@@ -1756,6 +1825,7 @@ static void usage(const char *cmd)
fprintf(stderr, "\tkey_id: an ID from 0 to 7\n");
fprintf(stderr,
"\tcipher: cipher to use, supported: aes (AES-GCM), chachapoly (CHACHA20POLY1305)\n");
+ fprintf(stderr, "\tkey_type: key type, supported: direct, epoch\n");
fprintf(stderr,
"\tkey_dir: key direction, must 0 on one host and 1 on the other\n");
fprintf(stderr, "\tkey_file: file containing the pre-shared key\n");
@@ -2247,12 +2317,19 @@ static int ovpn_parse_cmd_args(struct ovpn_ctx *ovpn, int argc, char *argv[])
}
if (argc > 7) {
+ if (argc < 9)
+ return -EINVAL;
+
ovpn->key_slot = OVPN_KEY_SLOT_PRIMARY;
ovpn->key_id = 0;
ovpn->cipher = OVPN_CIPHER_ALG_AES_GCM;
ovpn->key_dir = KEY_DIR_OUT;
- ret = ovpn_parse_key(argv[7], ovpn);
+ ret = ovpn_parse_key_type(argv[7], ovpn);
+ if (ret < 0)
+ return -1;
+
+ ret = ovpn_parse_key(argv[8], ovpn);
if (ret)
return -1;
}
@@ -2351,7 +2428,7 @@ static int ovpn_parse_cmd_args(struct ovpn_ctx *ovpn, int argc, char *argv[])
}
break;
case CMD_NEW_KEY:
- if (argc < 9)
+ if (argc < 10)
return -EINVAL;
ovpn->peer_id = strtoul(argv[3], NULL, 10);
@@ -2374,11 +2451,15 @@ static int ovpn_parse_cmd_args(struct ovpn_ctx *ovpn, int argc, char *argv[])
if (ret < 0)
return -1;
- ret = ovpn_parse_key_direction(argv[7], ovpn);
+ ret = ovpn_parse_key_type(argv[7], ovpn);
+ if (ret < 0)
+ return -1;
+
+ ret = ovpn_parse_key_direction(argv[8], ovpn);
if (ret < 0)
return -1;
- ret = ovpn_parse_key(argv[8], ovpn);
+ ret = ovpn_parse_key(argv[9], ovpn);
if (ret)
return -1;
break;
@@ -2442,6 +2523,7 @@ int main(int argc, char *argv[])
memset(&ovpn, 0, sizeof(ovpn));
ovpn.sa_family = AF_UNSPEC;
ovpn.cipher = OVPN_CIPHER_ALG_NONE;
+ ovpn.key_type = KEY_TYPE_DIRECT;
ovpn.cmd = ovpn_parse_cmd(argv[1]);
if (ovpn.cmd == CMD_INVALID) {
new file mode 100755
@@ -0,0 +1,11 @@
+#!/bin/bash
+# SPDX-License-Identifier: GPL-2.0
+# Copyright (C) 2026 OpenVPN, Inc.
+#
+# Author: Ralf Lici <ralf@mandelbit.com>
+# Antonio Quartulli <antonio@openvpn.net>
+
+OVPN_KEY_TYPE="epoch"
+OVPN_PROTO="TCP"
+
+source test.sh
new file mode 100755
@@ -0,0 +1,10 @@
+#!/bin/bash
+# SPDX-License-Identifier: GPL-2.0
+# Copyright (C) 2026 OpenVPN, Inc.
+#
+# Author: Ralf Lici <ralf@mandelbit.com>
+# Antonio Quartulli <antonio@openvpn.net>
+
+OVPN_KEY_TYPE="epoch"
+
+source test.sh
@@ -43,7 +43,8 @@ ovpn_mark_prepare_network() {
for p in $(seq 1 3); do
ovpn_cmd_ok "install server key for peer ${p}" \
ip netns exec ovpn_peer0 "${OVPN_CLI}" new_key tun0 \
- "${p}" 1 0 "${OVPN_ALG}" 0 data64.key
+ "${p}" 1 0 "${OVPN_ALG}" "${OVPN_KEY_TYPE}" \
+ 0 data64.key
done
for p in $(seq 1 3); do
@@ -67,14 +67,15 @@ ovpn_run_basic_traffic() {
local tcpdump_timeout="1.5s"
for p in $(seq 1 ${OVPN_NUM_PEERS}); do
- # The first part of the data packet header consists of:
+ # The tcpdump filters match the cleartext data packet prefix:
# - TCP only: 2 bytes for the packet length
# - 5 bits for opcode ("9" for DATA_V2)
# - 3 bits for key-id ("0" at this point)
- # - 12 bytes for peer-id:
+ # - 24 bits for peer-id:
# - with asymmetric ID: "${p}" one way and "${p} + 9" the
# other way
# - with symmetric ID: "${p}" both ways
+ # - epoch keys only: 2 bytes for epoch ("1" at this point)
header1=$(printf "0x4800000%x" ${p})
header2=$(printf "0x4800000%x" $((p + OVPN_ID_OFFSET)))
raddr=""
@@ -152,11 +153,12 @@ ovpn_run_key_rollover() {
peer_ns="ovpn_peer${p}"
ovpn_cmd_ok "add secondary key on peer0 for peer ${p}" \
ip netns exec ovpn_peer0 ${OVPN_CLI} new_key tun0 \
- ${p} 2 1 ${OVPN_ALG} 0 data64.key
+ ${p} 2 1 ${OVPN_ALG} ${OVPN_KEY_TYPE} \
+ 0 data64.key
ovpn_cmd_ok "add secondary key on peer${p} for peer ${p}" \
ip netns exec "${peer_ns}" ${OVPN_CLI} new_key tun${p} \
- $((p + OVPN_ID_OFFSET)) 2 1 ${OVPN_ALG} 1 \
- data64.key
+ $((p + OVPN_ID_OFFSET)) 2 1 ${OVPN_ALG} \
+ ${OVPN_KEY_TYPE} 1 data64.key
ovpn_cmd_ok "swap keys on peer${p}" \
ip netns exec "${peer_ns}" ${OVPN_CLI} swap_keys \
tun${p} $((p + OVPN_ID_OFFSET))