Database Tunneling

14 Jun 2019
Tags: bugfix networking relays

In order to run a database client locally, a SSH tunnel was made to a remote host. However, the connection from localhost to the database service was refused, while hosts on the same remote subnet were able to connect.

Analysis

The tunnel was started from local_client, connecting to the SSH server at remote_entrypoint, and forwarding traffic to the database host remote_database (with IP 10.55.55.5):

ssh foo@remote_entrypoint -L 1521:10.55.55.5:1521 -N -vvv

We can ping remote_database from local_client, and we know port 1521 was open by the Oracle SQL database server, by connecting with jdbc-tester from remote_entrypoint:

java -jar jdbc-tester-1.0.jar foo_user foo_pass 'jdbc:oracle:thin:@10.55.55.5:1521/FOO_SERVICE'

But the same test failed from local_client:

java -jar jdbc-tester-1.0.jar foo_user foo_pass 'jdbc:oracle:thin:@127.0.0.1:1521/FOO_SERVICE'

The service name also seemed to be recognized. Another way to test it would be with a TNS entry. To resolve FOO_SERVICE in localhost, we change the host referenced in tnsnames.ora:

FOO_LOCAL =
 (DESCRIPTION =
   (ADDRESS_LIST =
     (ADDRESS = (PROTOCOL = TCP)(HOST = localhost)(PORT = 1521))
   )
 (CONNECT_DATA =
   (SERVICE_NAME = FOO_SERVICE)
 )
)

Then we check if the database listener is available with McTnsping (error 0 is returned, which encodes success):

./McTnsping.exe -dir tnsnames/ -t FOO_LOCAL

Database clients were also complaining about network issues:

DBeaver:

DBeaver error

SQLcl:

./sql.exe 'foo_user/foo_pass@jdbc:oracle:thin:@//localhost:1521/FOO_SERVICE'

# SQLcl: Release 19.1 Production on Fri Jun 14 17:17:51 2019
# 
# Copyright (c) 1982, 2019, Oracle.  All rights reserved.
# 
#   USER          = foo_user
#   URL           = jdbc:oracle:thin:@//localhost:1521/FOO_SERVICE
#   Error Message = IO Error: The Network Adapter could not establish the connection
# Username? (RETRYING) ('foo_user/*********@jdbc:oracle:thin:@//localhost:1521/FOO_SERVICE'?)

SQL*Plus:

./sqlplus.exe 'foo_user/foo_pass@(DESCRIPTION = (ADDRESS_LIST = (ADDRESS = (PROTOCOL = TCP)(HOST = localhost)(PORT = 1521))) (CONNECT_DATA = (SERVICE_NAME = FOO_SERVICE)))'

# SQL*Plus: Release 19.0.0.0.0 - Production on Fri Jun 14 17:19:12 2019
# Version 19.3.0.0.0
# 
# Copyright (c) 1982, 2019, Oracle.  All rights reserved.
# 
# ERROR:
# ORA-12170: TNS:Connect timeout occurred
# 
# 
# Enter user-name:
# ERROR:
# ORA-12560: TNS:protocol adapter error
# 
# 
# Enter user-name:
# ERROR:
# ORA-12560: TNS:protocol adapter error
# 
# 
# SP2-0157: unable to CONNECT to ORACLE after 3 attempts, exiting SQL*Plus

Show me the bytes

The actual network traffic should allow us to see what was different between connections from local_client and remote_entrypoint.

Unfortunately, user foo at remote_entrypoint wasn’t an admin, so running tcpdump was out.

An alternative would be to forward TCP traffic from remote_entrypoint:1521 to remote_database:1521, and have local_client connect to the former. This way, we could run an unprivileged application that dumps its forwarded traffic.

One way to accomplish that would be with 2 instances of netcat and a FIFO pipe (optionally storing the processed input and output in files):

mknod backpipe p && ( \
    trap 'rm -f backpipe' EXIT INT QUIT TERM && \
    nc -kl 127.0.0.1 1521 < backpipe | \
    tee -a 'in.txt' | \
    nc 10.55.55.5 1521 | \
    tee -a 'out.txt' > backpipe \
)

However I decided to use socat for this, as I preferred the formatted dumps it makes. After statically compiling it and moving it to remote_entrypoint:

./socat -v -x -d -d TCP4-LISTEN:1521,reuseaddr,fork TCP4:10.55.55.5:1521

Then on local_client:

ssh foo@remote_entrypoint -L 1521:127.0.0.1:1521 -N -vvv

Now we get the luxury of having error codes:

java -jar jdbc-tester-1.0.jar foo_user foo_pass 'jdbc:oracle:thin:@127.0.0.1:1521/FOO_SERVICE'

# > 2019/06/14 15:38:28.930225  length=282 from=0 to=281
# .........6.,\fA ...O........:..............................(DESCRIPTION=(ADDRESS=(PROTOCOL=TCP)(HOST=127.0.0.1)(PORT=1521))(CONNECT_DATA=(CID=(PROGRAM=JDBC Thin Client)(HOST=__jdbc__)(USER=foo))(SERVICE_NAME=FOO_SERVICE)(CID=(PROGRAM=JDBC Thin Client)(HOST=__jdbc__)(USER=foo))))
# < 2019/06/14 15:38:28.940604  length=103 from=0 to=102
# .g......"..[(DESCRIPTION=(TMP=)(VSNNUM=301989888)(ERR=12516)(ERROR_STACK=(ERROR=(CODE=12516)(EMFI=4))))

But let’s look at the actual response of the database server:

./sql.exe 'foo_user/foo_pass@jdbc:oracle:thin:@127.0.0.1:1521/FOO_SERVICE'

# > 2019/06/14 15:39:57.860357  length=230 from=0 to=229
# .........=.,\fA ...O........F................................ .. ......(DESCRIPTION=(ADDRESS=(PROTOCOL=TCP)(HOST=127.0.0.1)(PORT=1521))(CONNECT_DATA=(CID=(PROGRAM=SQLcl)(HOST=__jdbc__)(USER=local_foo))(SERVICE_NAME=FOO_SERVICE)))
# < 2019/06/14 15:39:57.861883  length=276 from=0 to=275
# .
# .........
# .......@(ADDRESS=(PROTOCOL=TCP)(HOST=10.55.55.2)(PORT=1521)).(DESCRIPTION=(ADDRESS=(PROTOCOL=TCP)(HOST=127.0.0.1)(PORT=1521))(CONNECT_DATA=(CID=(PROGRAM=SQLcl)(HOST=__jdbc__)(USER=local_foo))(SERVICE_NAME=FOO_SERVICE)(SERVER=dedicated)(INSTANCE_NAME=FOO_SERVICE2)))
./sqlplus.exe 'foo_user/foo_pass@(DESCRIPTION = (ADDRESS_LIST = (ADDRESS = (PROTOCOL = TCP)(HOST = localhost)(PORT = 1521))) (CONNECT_DATA = (SERVICE_NAME = FOO_SERVICE)))'

# > 2019/06/14 15:42:25.934838  length=323 from=0 to=322
# .J.......>.,\fA ............J....AA.......................... .. ....................(DESCRIPTION=(ADDRESS=(PROTOCOL=TCP)(HOST=127.0.0.1)(PORT=1521)(HOSTNAME=localhost))(CONNECT_DATA=(SERVICE_NAME=FOO_SERVICE)(CID=(PROGRAM=C:\\Users\\local_foo\\Downloads\\instantclient_19_3\\sqlplus.exe)(HOST=local_client)(USER=local_foo))))
# < 2019/06/14 15:42:25.936677  length=10 from=0 to=9
# .
# .
# .....O
# < 2019/06/14 15:42:25.936831  length=345 from=10 to=354
# .Y.
# .....@(ADDRESS=(PROTOCOL=TCP)(HOST=10.55.55.2)(PORT=1521)).(DESCRIPTION=(ADDRESS=(PROTOCOL=TCP)(HOST=127.0.0.1)(PORT=1521)(HOSTNAME=localhost))(CONNECT_DATA=(SERVICE_NAME=FOO_SERVICE)(CID=(PROGRAM=C:\\Users\\local_foo\\Downloads\\instantclient_19_3\\sqlplus.exe)(HOST=local_client)(USER=local_foo))(SERVER=dedicated)(INSTANCE_NAME=FOO_SERVICE2)))

The response comes from another IP: 10.55.55.2! Oracle SQL can be configured to run multiple instances for the same database. Our request from local_client was passing through without issues, but we only had a SSH tunnel for 10.55.55.5. It turns out that we can just connect to the instance directly and have our clients working from local_client:

ssh foo@remote_entrypoint -L 1521:10.55.55.2:1521 -N -vvv

Further work

A simple improvement for database clients: when a network connection fails, dump the remote hostname or IP of that connection attempt! Omitting this information misleads the user into assuming the database is self-contained on a single host.