Connect databases through a static IP
Database drivers do not speak HTTP CONNECT. Send Postgres, MySQL, MongoDB, and Redis connections through your static IPs with a SOCKS5 dialer, or with the planned bm tunnel CLI for drivers that have no proxy option.
Docs preview: Fixedmark is not live yet. Endpoints are issued at launch, and these pages describe planned launch behavior. Anything marked planned may change. Join early access to get yours first.
SOCKS5 or bm tunnel
- SOCKS5 works inside your process, so it fits serverless functions. The driver needs a proxy option or a custom dial function. Use
FIXEDMARK_SOCKS_URLwith thesocks5h://scheme. - bm tunnel (planned) forwards a local port to the database through your static IPs. Any driver works, because it just connects to
127.0.0.1. It needs a long-lived process next to your app, so it fits Railway, Render, Fly.io, Heroku, VMs, and containers. See the bm tunnel reference.
Both carry raw TCP. Your database's own TLS runs end to end, and the proxy never sees queries or results. Allowlist both IPs of your pair in the database firewall first.
Driver support
| Driver | SOCKS5 | Use |
|---|---|---|
| Go pgx | Custom DialFunc | SOCKS5 dialer |
| Go go-sql-driver/mysql | RegisterDialContext | SOCKS5 dialer |
| Go go-redis | Custom Dialer | SOCKS5 dialer plus TLS |
| MongoDB Node.js driver | proxyHost options | SOCKS5 options |
| libpq, psql, psycopg | None | bm tunnel |
| node-postgres (pg) | None built in | bm tunnel |
| mysql2, PyMySQL, PDO | None built in | bm tunnel |
| ioredis, redis-py | None built in | bm tunnel |
Postgres
In Go, set pgx's DialFunc to the SOCKS5 dialer from the Go guide. pgx still verifies TLS against the host name in DATABASE_URL.
libpq-based clients and node-postgres have no SOCKS5 option. Run bm tunnel and connect to 127.0.0.1. With sslmode=verify-full, keep the real name in host and put the tunnel address in hostaddr, so the certificate check uses the real name.
// Uses socksDialer() from the Go guide.
func connectPostgres(ctx context.Context) (*pgxpool.Pool, error) {
dialer, err := socksDialer()
if err != nil {
return nil, err
}
cfg, err := pgxpool.ParseConfig(os.Getenv("DATABASE_URL"))
if err != nil {
return nil, err
}
// pgx still runs TLS against the host name in DATABASE_URL.
cfg.ConnConfig.DialFunc = dialer.DialContext
return pgxpool.NewWithConfig(ctx, cfg)
}# Planned: bm tunnel 5432:db.example.com:5432 running beside your app.
# host= is the name checked against the server certificate.
# hostaddr= is where libpq actually connects: the local tunnel.
psql "host=db.example.com hostaddr=127.0.0.1 port=5432 \
dbname=app user=app sslmode=verify-full"// Planned: bm tunnel 5432:db.example.com:5432 running beside your app.
import pg from "pg";
const pool = new pg.Pool({
host: "127.0.0.1",
port: 5432,
user: "app",
password: process.env.DB_PASSWORD,
database: "app",
// Check the certificate against the real server name, not 127.0.0.1.
ssl: { servername: "db.example.com" },
// Close idle clients before the proxy's idle timeout (planned: 5 minutes).
idleTimeoutMillis: 60_000,
});Managed Postgres such as AWS RDS and Cloud SQL take the pair as two /32 rules. Self-hosted servers need both IPs in pg_hba.conf and the firewall. See Postgres and MySQL allowlisting.
MySQL
go-sql-driver/mysql lets you register a named dial function. The network name in the DSN selects it. Other MySQL drivers need bm tunnel. Through a tunnel, VERIFY_IDENTITY compares the certificate with 127.0.0.1 and fails. Use VERIFY_CA, or a driver option that sets the expected server name.
// Uses socksDialer() from the Go guide.
func openMySQL() (*sql.DB, error) {
dialer, err := socksDialer()
if err != nil {
return nil, err
}
mysql.RegisterDialContext("fixedmark",
func(ctx context.Context, addr string) (net.Conn, error) {
return dialer.DialContext(ctx, "tcp", addr)
})
// The network name in the DSN selects the dialer:
// app:secret@fixedmark(mysql.example.com:3306)/app?tls=true
return sql.Open("mysql", os.Getenv("MYSQL_DSN"))
}# Planned: bm tunnel 3306:mysql.example.com:3306 running beside your app.
# VERIFY_IDENTITY would compare the certificate with 127.0.0.1 and fail.
# VERIFY_CA still checks the certificate chain.
mysql --host=127.0.0.1 --port=3306 --user=app -p \
--ssl-mode=VERIFY_CA --ssl-ca=ca.pem appMongoDB
The MongoDB Node.js driver supports SOCKS5 through the proxyHost, proxyPort, proxyUsername, and proxyPassword options. Install the socks package next to it. The driver sends host names to the proxy, so each replica set member resolves on the proxy side.
// npm install mongodb socks
import { MongoClient } from "mongodb";
const socks = new URL(process.env.FIXEDMARK_SOCKS_URL);
const client = new MongoClient(process.env.MONGODB_URI, {
proxyHost: socks.hostname,
proxyPort: Number(socks.port),
proxyUsername: decodeURIComponent(socks.username),
proxyPassword: decodeURIComponent(socks.password),
});
await client.connect();With a mongodb+srv:// URI, the driver still looks up the SRV and TXT records locally. Only the TCP connections go through the proxy, which is what Atlas's IP access list checks. A tunnel is a poor fit for replica sets, because each member needs its own forwarded port. See MongoDB Atlas allowlisting.
Redis
go-redis takes a custom Dialer. When you set one, go-redis no longer adds TLS itself, so wrap the connection in TLS in the dialer for rediss:// URLs. Other clients need bm tunnel. Pass the real host name for TLS SNI.
// Uses socksDialer() from the Go guide. go-redis skips its own TLS
// when you set Dialer, so wrap the connection in TLS here.
func newRedis() (*redis.Client, error) {
dialer, err := socksDialer()
if err != nil {
return nil, err
}
opt, err := redis.ParseURL(os.Getenv("REDIS_URL")) // rediss://...
if err != nil {
return nil, err
}
tlsCfg := opt.TLSConfig
opt.Dialer = func(ctx context.Context, network, addr string) (net.Conn, error) {
conn, err := dialer.DialContext(ctx, network, addr)
if err != nil || tlsCfg == nil {
return conn, err
}
return tls.Client(conn, tlsCfg), nil
}
return redis.NewClient(opt), nil
}# Planned: bm tunnel 6380:redis.example.com:6380 running beside your app.
# Pass the real host name for TLS SNI and certificate checks.
redis-cli -h 127.0.0.1 -p 6380 --tls --sni redis.example.com PINGSSH, SFTP, and other TCP
OpenSSH can dial through SOCKS5 with a ProxyCommand, but it has no built-in SOCKS5 password support. Forward a port with the planned bm tunnel instead, and keep host key checks tied to the real host name.
# Planned: bm tunnel 2222:server.example.com:22
ssh -p 2222 -o HostKeyAlias=server.example.com deploy@127.0.0.1Pools and timeouts
- Each new database connection is one proxy request. Pools keep connections open, so usage stays low.
- The proxy closes idle connections after a timeout, planned at 5 minutes by default and configurable for database tunnels. Set the pool's idle timeout below it.
- During failover, open connections on the failed node drop. Make sure your pool checks connections before use or retries on reset.
- Serverless functions open new connections often. Keep a pool at module scope so warm invocations reuse it.
Platform guides
Frequently asked questions
How do I connect to an IP-restricted database from serverless?
Allowlist both Fixedmark IPs in the database firewall. Then dial through SOCKS5 if your driver supports a custom dialer or proxy option, or run the planned bm tunnel on a platform with long-lived processes.
Does the database TLS still work through the proxy?
Yes. SOCKS5 and bm tunnel carry raw TCP, so the driver negotiates TLS with the database end to end. Through a local tunnel, tell the driver the real server name so certificate checks pass.
Why do idle database connections drop?
The proxy closes idle connections after a timeout, planned at 5 minutes by default. Set your pool's idle timeout lower, or enable TCP keepalive, so the pool replaces connections before the proxy closes them.
Can I use bm tunnel in Vercel or Lambda functions?
No. Functions cannot run a background process. Use a driver with SOCKS5 support there, such as the MongoDB Node.js driver or Go drivers with a custom dialer.
Make it fixed.
Fixedmark is in early access. Join the list to get dedicated static IPs when your region opens.