From b722111eebdcc03536c7ea0bdee436785cfa180f Mon Sep 17 00:00:00 2001 From: Tatsuo Ishii Date: Sat, 10 Oct 2026 16:47:10 +0900 Subject: [PATCH v3 2/2] Modify pgproto as a test driver. This commit adds new option "-t" ("--test-v2" for long option), which sends a v2 protocol startup packet and read reply, then exit. This is used to test Pgpool-II to see if they reject a v2 protocol connection request as expected. Add 045.remove_proto_v2 regression test to check v2 protocol is rejected. Author: Tatsuo Ishii Reviewed-by: Nadav Shatz Discussion: https://www.postgresql.org/message-id/20260625.151428.1666276852188903568.ishii%40postgresql.org Backpatch-through: master only --- doc.ja/src/sgml/ref/pgproto.sgml | 11 + doc/src/sgml/ref/pgproto.sgml | 12 + src/include/pgproto/pgproto.h | 3 +- src/include/pgproto/read.h | 7 +- src/include/pgproto/send.h | 4 +- .../tests/045.remove_proto_v2/expected.txt | 2 + .../tests/045.remove_proto_v2/test.sh | 39 +++ src/tools/pgproto/main.c | 261 +++++++++++++++++- src/tools/pgproto/read.c | 24 +- src/tools/pgproto/send.c | 18 +- 10 files changed, 356 insertions(+), 25 deletions(-) create mode 100644 src/test/regression/tests/045.remove_proto_v2/expected.txt create mode 100755 src/test/regression/tests/045.remove_proto_v2/test.sh diff --git a/doc.ja/src/sgml/ref/pgproto.sgml b/doc.ja/src/sgml/ref/pgproto.sgml index 4567883ce..7f840d2e8 100644 --- a/doc.ja/src/sgml/ref/pgproto.sgml +++ b/doc.ja/src/sgml/ref/pgproto.sgml @@ -138,6 +138,17 @@ Pgpool-II documentation + + + + + + v2プロトコルのスタートアップパケットを送信し、返信を読み込んだ後終了します。 + これはPgpool-II 4.8以後のバージョン (そしてPostgreSQL 14以後のバージョン)が、v2プロトコルの接続要求を拒否するかどうかのテストに使用します。 + + + + diff --git a/doc/src/sgml/ref/pgproto.sgml b/doc/src/sgml/ref/pgproto.sgml index 35e17d530..79772022d 100644 --- a/doc/src/sgml/ref/pgproto.sgml +++ b/doc/src/sgml/ref/pgproto.sgml @@ -101,6 +101,18 @@ Pgpool-II documentation + + + + + + Send a v2 protocol startup packet and read reply, then exit. This is + used to test Pgpool-II 4.8 or later (and PostgreSQL 14 or later) to see + if they reject a v2 protocol connection request. + + + + diff --git a/src/include/pgproto/pgproto.h b/src/include/pgproto/pgproto.h index 9be981572..314506197 100644 --- a/src/include/pgproto/pgproto.h +++ b/src/include/pgproto/pgproto.h @@ -1,5 +1,6 @@ /* - * Copyright (c) 2017-2018 Tatsuo Ishii + * Portions Copyright (c) 2018-2026 PgPool Global Development Group + * Portions Copyright (c) 2017-2018 Tatsuo Ishii * * Permission to use, copy, modify, and distribute this software and * its documentation for any purpose and without fee is hereby diff --git a/src/include/pgproto/read.h b/src/include/pgproto/read.h index c7f2dd0ce..b0652d422 100644 --- a/src/include/pgproto/read.h +++ b/src/include/pgproto/read.h @@ -1,5 +1,6 @@ /* - * Copyright (c) 2017 Tatsuo Ishii + * Portions Copyright (c) 2018-2026 PgPool Global Development Group + * Portions Copyright (c) 2017-2018 Tatsuo Ishii * * Permission to use, copy, modify, and distribute this software and * its documentation for any purpose and without fee is hereby @@ -16,5 +17,9 @@ #ifndef READ_H #define READ_H +#include + extern void read_until_ready_for_query(PGconn *conn, int check_input, int wait_for_ready_for_query); +extern char *read_string_internal(int fd); + #endif diff --git a/src/include/pgproto/send.h b/src/include/pgproto/send.h index 9ce8d9dcc..0c41f2435 100644 --- a/src/include/pgproto/send.h +++ b/src/include/pgproto/send.h @@ -1,5 +1,6 @@ /* - * Copyright (c) 2017 Tatsuo Ishii + * Portions Copyright (c) 2018-2026 PgPool Global Development Group + * Portions Copyright (c) 2017-2018 Tatsuo Ishii * * Permission to use, copy, modify, and distribute this software and * its documentation for any purpose and without fee is hereby @@ -21,5 +22,6 @@ extern void send_int(int intval, PGconn *conn); extern void send_int16(short shortval, PGconn *conn); extern void send_string(char *buf, PGconn *conn); extern void send_byte(char *buf, int len, PGconn *conn); +extern void write_it(int fd, void *buf, int len); #endif diff --git a/src/test/regression/tests/045.remove_proto_v2/expected.txt b/src/test/regression/tests/045.remove_proto_v2/expected.txt new file mode 100644 index 000000000..009b8d478 --- /dev/null +++ b/src/test/regression/tests/045.remove_proto_v2/expected.txt @@ -0,0 +1,2 @@ +<= BE ErrorResponse(FATAL: unsupported frontend protocol 2.0 +) diff --git a/src/test/regression/tests/045.remove_proto_v2/test.sh b/src/test/regression/tests/045.remove_proto_v2/test.sh new file mode 100755 index 000000000..f8bfcdb99 --- /dev/null +++ b/src/test/regression/tests/045.remove_proto_v2/test.sh @@ -0,0 +1,39 @@ +#!/usr/bin/env bash +#------------------------------------------------------------------- +# Test script for v2 protocol acceptance. +# From Pgpool-II 4.8, it does not accept v2 frontend/backend protocol anymore. +# This test simply sends a v2 startup packet to pgpool using pgproto +# and see if pgpool rejects it. +# Note that this test requires version 4.8 or later Pgpool-II +# (including pgproto) and PostgreSQL 14 or later. +# +# +source $TESTLIBS +TESTDIR=testdir +PSQL=$PGBIN/psql +PG_CTL=$PGBIN/pg_ctl +PGPROTO=$PGPOOL_INSTALL_DIR/bin/pgproto +export PGDATABASE=test +export PGUSER=$USER + +rm -fr $TESTDIR +mkdir $TESTDIR +cd $TESTDIR + +echo -n "creating test environment..." +$PGPOOL_SETUP || exit 1 +echo "done." +source ./bashrc.ports +./startall +wait_for_pgpool_startup + +$PGPROTO -d $PGDATABASE -h localhost -p $PGPOOL_PORT --test-v2 > result.txt 2>&1 +diff ../expected.txt result.txt +if [ $? = 0 ];then + echo "test Ok." + ./shutdownall + exit 0 +fi +echo "The expected result was different." +./shutdownall +exit 1 diff --git a/src/tools/pgproto/main.c b/src/tools/pgproto/main.c index 76e272a9a..c7fbc47bf 100644 --- a/src/tools/pgproto/main.c +++ b/src/tools/pgproto/main.c @@ -1,6 +1,6 @@ /* - * Copyright (c) 2017-2018 Tatsuo Ishii - * Copyright (c) 2018-2026 PgPool Global Development Group + * Portions Copyright (c) 2018-2026 PgPool Global Development Group + * Portions Copyright (c) 2017-2018 Tatsuo Ishii * * Permission to use, copy, modify, and distribute this software and * its documentation for any purpose and without fee is hereby @@ -23,6 +23,12 @@ #include #include #include +#include +#include +#include +#include +#include +#include #include "pgproto/fe_memutils.h" #include #include "pgproto/read.h" @@ -40,6 +46,9 @@ static void read_and_process(FILE *fd, PGconn *conn); static int process_a_line(char *buf, PGconn *con); static int process_message_type(int kind, char *buf, PGconn *conn); static void process_function_call(char *buf, PGconn *conn); +static void connect_v2(char *host, char *port, char *user, char *database); +static int connect_unix_domain_socket_by_port(int port, char *socket_dir); +static int connect_inet_domain_socket_by_port(char *host, char *port); int read_nap = 0; @@ -58,6 +67,7 @@ main(int argc, char **argv) FILE *fd; PGconn *con; int var; + bool test_v2 = false; static struct option long_options[] = { {"host", optional_argument, NULL, 'h'}, @@ -69,6 +79,7 @@ main(int argc, char **argv) {"help", no_argument, NULL, '?'}, {"version", no_argument, NULL, 'v'}, {"read-nap", optional_argument, NULL, 'r'}, + {"test-v2", no_argument, NULL, 't'}, {NULL, 0, NULL, 0} }; @@ -81,7 +92,7 @@ main(int argc, char **argv) if ((env = getenv("PGUSER")) != NULL && *env != '\0') user = env; - while ((opt = getopt_long(argc, argv, "v?Dh:p:u:d:f:r:", long_options, &optindex)) != -1) + while ((opt = getopt_long(argc, argv, "v?Dh:p:u:d:f:r:t", long_options, &optindex)) != -1) { switch (opt) { @@ -123,12 +134,26 @@ main(int argc, char **argv) read_nap = atoi(optarg); break; + case 't': + test_v2 = true; + break; + default: usage(); exit(1); } } + if (test_v2) + { + /* + * test_v2 is specified. Send a v2 startup packet and see if it's + * actually rejected. We expect all existing (supported) backend + * rejects v2 protocol. Read the error message and show it. + */ + connect_v2(host, port, user, database); + exit(0); + } fd = openfile(data_file); con = connect_db(host, port, user, database); var = fcntl(PQsocket(con), F_GETFL, 0); @@ -168,6 +193,7 @@ usage(void) "-d, --database DATABASENAME (default: same as user)\n" "-f, --proto-data-file FILENAME (default: pgproto.data)\n" "-r, --read-nap NAPTIME (in micro seconds. default: 0)\n" + "-t, --test-v2\n" "-D, --debug\n" "-?, --help\n" "-v, --version\n", @@ -462,7 +488,7 @@ process_message_type(int kind, char *buf, PGconn *conn) process_function_call(buf, conn); break; - case 's': /* frontend sleep request */ + case 's': /* frontend sleep request */ SKIP_TABS(buf); stime = buffer_read_int(buf, &bufp); fprintf(stderr, "FE=> Sleep %ld\n", stime); @@ -622,3 +648,230 @@ process_function_call(char *buf, PGconn *conn) /* result format code */ send_int16(result_formatcode, conn); } + +/* + * connect_v2 + * + * Send a v2 startup packet and see if it's actually rejected. We expect all + * existing (supported) backend rejects v2 protocol. Read the error message + * and show it to stderr. + */ +static void +connect_v2(char *host, char *port, char *user, char *database) +{ + int fd = -1; + int port_val; + int v2_proto_version_h = 0x00020000; /* V2 protocol version in + * host order */ + int len; + int nlen; + char buf1[64]; + char buf2[32]; + char kind; + char *socket_dir = "/tmp"; + char *p; + + port_val = atoi(port); + if (port_val == 0) + { + fprintf(stderr, "invalid port \"%s\" specified\n", port); + exit(1); + } + + if (user == NULL || *user == '\0') + { + fprintf(stderr, "no user name specified\n"); + exit(1); + } + + if (database == NULL || *database == '\0') + database = strdup(user); + + if (host == NULL || *host == '\0' || *host == '/') + { + if (*host == '/') + socket_dir = host; + fd = connect_unix_domain_socket_by_port(port_val, socket_dir); + if (fd < 0) + exit(1); + } + else + { + fd = connect_inet_domain_socket_by_port(host, port); + } + + /* Create and send v2 protocol startup packet */ + len = sizeof(int); /* self lenghth */ + len += sizeof(int); /* protocol version */ + len += 64; /* database */ + len += 32; /* user */ + len += 64; /* options */ + len += 64; /* unused */ + len += 64; /* tty */ + + nlen = htonl(len); + write_it(fd, &nlen, sizeof(int)); /* message length including self */ + nlen = htonl(v2_proto_version_h); + write_it(fd, &nlen, sizeof(int)); /* protocol version */ + + memset(buf1, 0, sizeof(buf1)); + strlcpy(buf1, database, sizeof(buf1)); + write_it(fd, buf1, sizeof(buf1)); /* database */ + + memset(buf2, 0, sizeof(buf2)); + strlcpy(buf2, user, sizeof(buf2)); + write_it(fd, buf2, sizeof(buf2)); /* user */ + + memset(buf1, 0, sizeof(buf1)); + write_it(fd, buf1, sizeof(buf1)); /* options */ + + memset(buf1, 0, sizeof(buf1)); + write_it(fd, buf1, sizeof(buf1)); /* unused area */ + + memset(buf1, 0, sizeof(buf1)); + write_it(fd, buf1, sizeof(buf1)); /* tty */ + + /* read response from backend */ + len = read(fd, &kind, sizeof(char)); + if (len < 1) + fprintf(stderr, "canot read backend response message kind \"%m\"\n"); + if (kind == 'E') + fprintf(stderr, "<= BE ErrorResponse("); + else + { + fprintf(stderr, "unknown response %d\n", kind); + exit(1); + } + + p = read_string_internal(fd); /* read v2 error response */ + fprintf(stderr, "%s", p); /* print it */ + fprintf(stderr, ")\n"); + + exit(0); +} + +/* + * connect_unix_domain_socket_by_port + * + * Connect to PostgreSQL server by using UNIX domain socket. + */ +static int +connect_unix_domain_socket_by_port(int port, char *socket_dir) +{ + struct sockaddr_un addr; + int fd; + int len; + + if (socket_dir == NULL) + socket_dir = "/tmp"; + + fd = socket(AF_UNIX, SOCK_STREAM, 0); + if (fd == -1) + { + fprintf(stderr, "failed to connect to PostgreSQL server by unix domain socket \"%m\""); + return -1; + } + + memset((char *) &addr, 0, sizeof(addr)); + addr.sun_family = AF_UNIX; + snprintf(addr.sun_path, sizeof(addr.sun_path), "%s/.s.PGSQL.%d", socket_dir, port); + len = sizeof(struct sockaddr_un); + +retry: + if (connect(fd, (struct sockaddr *) &addr, len) < 0) + { + if (errno == EINTR || errno == EAGAIN) + goto retry; + close(fd); + fprintf(stderr, "failed to connect to PostgreSQL server by unix domain socket\n"); + fprintf(stderr, "connect to \"%s\" failed with error \"%m\"", addr.sun_path); + exit(1); + } + return fd; +} + +/* + * connect_inet_domain_socket_by_port + * + * Connect to PostgreSQL server by using INET domain socket. + */ +static int +connect_inet_domain_socket_by_port(char *host, char *port) +{ + int fd = -1; + int on = 1; + int ret; + struct addrinfo *res; + struct addrinfo *walk; + struct addrinfo hints; + int retry_cnt = 5; /* getaddrinfo() retry count in case EAI_AGAIN */ + + memset(&hints, 0, sizeof(struct addrinfo)); + hints.ai_family = PF_UNSPEC; + hints.ai_socktype = SOCK_STREAM; + + for (;;) + { + if ((ret = getaddrinfo(host, port, &hints, &res)) != 0) + { + if (ret != EAI_AGAIN) + { + fprintf(stderr, "failed to connect to PostgreSQL server, getaddrinfo() failed with error \"%s\"\n", + gai_strerror(ret)); + exit(1); + } + retry_cnt--; + + if (retry_cnt <= 0) + { + fprintf(stderr, "failed to connect to PostgreSQL server, getaddrinfo() failed due to retry count over"); + exit(1); + } + } + else /* getaddrinfo succeeded. */ + break; + } + + for (walk = res; walk != NULL; walk = walk->ai_next) + { + fd = socket(walk->ai_family, walk->ai_socktype, walk->ai_protocol); + if (fd < 0) + { + fprintf(stderr, "failed to connect to PostgreSQL server, socket() failed \"%m\"\n"); + continue; + } + + /* set nodelay */ + if (setsockopt(fd, IPPROTO_TCP, TCP_NODELAY, (char *) &on, + sizeof(on)) < 0) + { + fprintf(stderr, "failed to connect to PostgreSQL server, setsockopt() failed \"%m\"\n"); + close(fd); + freeaddrinfo(res); + exit(1); + } + +retry: + if (connect(fd, walk->ai_addr, walk->ai_addrlen) < 0) + { + if (errno == EINTR || errno == EAGAIN) + goto retry; + + else + { + fprintf(stderr, "failed to connect to PostgreSQL server on \"%s:%s\" \"%m\"\n", + host, port); + close(fd); + freeaddrinfo(res); + exit(1); + } + } + freeaddrinfo(res); + return fd; + } + fprintf(stderr, "failed to connect to PostgreSQL server\n"); + close(fd); + freeaddrinfo(res); + + exit(1); +} diff --git a/src/tools/pgproto/read.c b/src/tools/pgproto/read.c index 7dc44aa36..462c57c85 100644 --- a/src/tools/pgproto/read.c +++ b/src/tools/pgproto/read.c @@ -1,6 +1,6 @@ /* - * Copyright (c) 2017-2018 Tatsuo Ishii - * Copyright (c) 2018-2023 PgPool Global Development Group + * Portions Copyright (c) 2018-2023 PgPool Global Development Group + * Portions Copyright (c) 2017-2018 Tatsuo Ishii * * Permission to use, copy, modify, and distribute this software and * its documentation for any purpose and without fee is hereby @@ -372,6 +372,15 @@ read_it(PGconn *conn, char *buf, int len) */ static char * read_string(PGconn *conn) +{ + return read_string_internal(PQsocket(conn)); +} + +/* + * Read a string from fd and returns malloced buffer pointer. + */ +char * +read_string_internal(int fd) { #define PROTO_ALLOC_SIZE 512 @@ -387,20 +396,22 @@ read_string(PGconn *conn) for (;;) { - sts = read(PQsocket(conn), p, 1); +retry: + sts = read(fd, p, 1); if (sts == 0) { - fprintf(stderr, "read_string: EOF detected"); + fprintf(stderr, "read_string_internal: EOF detected"); exit(1); } else if (sts < 0) { - fprintf(stderr, "read_string: read(2) returns error %s\n", strerror(errno)); + if (errno == EINTR || errno == EAGAIN) + goto retry; + fprintf(stderr, "read_string_internal: read(2) returns error %m\n"); exit(1); } - if (*p == '\0') { return buf; @@ -416,5 +427,4 @@ read_string(PGconn *conn) len = PROTO_ALLOC_SIZE; } } - } diff --git a/src/tools/pgproto/send.c b/src/tools/pgproto/send.c index 2f359291b..77b2ab10b 100644 --- a/src/tools/pgproto/send.c +++ b/src/tools/pgproto/send.c @@ -1,6 +1,6 @@ /* - * Copyright (c) 2017-2018 Tatsuo Ishii - * Copyright (c) 2018-2021 PgPool Global Development Group + * Portions Copyright (c) 2018-2026 PgPool Global Development Group + * Portions Copyright (c) 2017-2018 Tatsuo Ishii * * Permission to use, copy, modify, and distribute this software and * its documentation for any purpose and without fee is hereby @@ -29,8 +29,6 @@ #include "pgproto/read.h" #include "pgproto/send.h" -static void write_it(int fd, void *buf, int len); - /* * Send a character to the connection. */ @@ -81,18 +79,16 @@ send_byte(char *buf, int len, PGconn *conn) } /* - * Wrapper for write(2). + * Wrapper for write(2) + * + * If write() fails, print an error message and exit. */ -static void write_it(int fd, void *buf, int len) { - int errsave = errno; - - errno = 0; if (write(fd, buf, len) < 0) { - fprintf(stderr, "write_it: warning write(2) failed: %s\n", strerror(errno)); + fprintf(stderr, "write_it: warning: write(2) failed: \"%m\"\n"); + exit(1); } - errno = errsave; } -- 2.43.0