Just launched - Fast Search API: organic SERP data in under 1 second Try it now

How to build a private proxy server with sing-box, VLESS, Hysteria2 and SOCKS on Linux

17 March 2026 | 37 min read

In this guide we will walk through how to build your own proxy server on Linux using sing-box with a few well-known protocols: SOCKS, VLESS with Reality, and Hysteria2.

The idea is to put together a small but flexible proxy setup that you control yourself. Instead of relying on some external service, you run the whole thing on your own VPS. That gives you more visibility into what is actually happening and helps you understand how modern proxy stacks work under the hood.

We will use Ubuntu as the base system since it is one of the most common Linux distributions on VPS providers. Everything will be configured step by step so it is easy to follow along.

While doing that we will also cover a few basic system hardening and networking steps, for example:

  • securing SSH access with key authentication
  • enabling a firewall with ufw
  • adding brute-force protection with fail2ban
  • setting up a local DNS resolver with unbound
  • enabling the BBR TCP congestion control algorithm

Once the base system is ready, we will install sing-box and start adding proxy protocols.

In this guide we're going to set up:

  • a SOCKS5 proxy that can be used easily in apps like Telegram or in browsers
  • VLESS with Reality, which is designed to look like normal TLS traffic
  • Hysteria2, a UDP based protocol that often performs well on unstable networks
  • optional obfuscation for Hysteria2 if you want an extra disguise layer

We will also look at simple client configurations and a few ways to watch logs so you can confirm everything works. The goal here is not to introduce some magic automation, but to show how the pieces fit together. By the end you should have a working multi-protocol proxy server and a pretty good idea of how the components interact.

How to build a private proxy server with sing-box, VLESS, Hysteria2 and SOCKS on Linux

Why you might want a proxy

There are plenty of reasons to run a personal proxy server. One of the most obvious ones is privacy.

Without a proxy, traffic usually goes straight through the local ISP or mobile provider to the final service. A proxy adds one more layer in the middle. Of course, it doesn't make anyone magically invisible, but it does create extra separation and gives more control over how traffic moves.

And honestly, wanting a normal level of privacy online doesn't need some grand explanation. It's a basic human thing: the right to private life, private communication, and personal space.

Other common use cases include:

  • Access while traveling: hotel Wi-Fi, airport networks, and random public hotspots often block or break certain services
  • Restricted networks: office, campus, or public connections sometimes block ports or filter traffic, and a proxy can push everything through one working path
  • More stable access: if routing to some regions is bad, bouncing traffic through a different server can make things more consistent
  • Scraping and automation: proxies are often used for scraping, data collection, and automation workflows, especially when requests need to come from different IPs or look less uniform. That said, running a personal proxy pool for production scraping gets complicated fast, so for that kind of workload a web scraping API like ScrapingBee can be a lot more practical since it already provides proxy infrastructure out of the box.
  • Development and testing: useful for checking how apps behave from another network path or for routing API traffic through a controlled endpoint
  • Access to personal infrastructure: can work as an entry point to services running on a server
  • Learning and experimentation: a setup like this is a solid way to understand Linux networking, DNS, encryption, and modern proxy protocols a bit better

A necessary disclaimer

Before going any further, I should make a few things clear.

  • First of all, everything in this guide is provided as-is. I cannot guarantee that these commands, configurations, or examples are suitable for your specific environment. Servers, networks, hosting providers, and operating systems can all behave differently. Always review what you run and understand the changes you make to your system.
  • Also I cannot guarantee any level of security with the setup shown here. Security depends on many factors: your server configuration, your provider, software updates, network conditions, and how the system is used over time. This post is meant to show how the technologies fit together, not to provide a production-grade security blueprint.
    • You are responsible for reviewing, testing, and adapting everything to your own environment.
  • Think of this guide as an educational overview. It is meant to demonstrate how tools like sing-box, VLESS, Hysteria2, SOCKS, and Linux networking can work together. It is not a "copy these commands and everything will be perfect" type of solution.
  • This tutorial is written purely out of engineering curiosity. I have worked as a systems administrator for quite a while, and at some point I simply wanted to explore how modern proxy stacks are built today. Technologies evolve, tools change, and it is always interesting to see how current solutions approach problems like routing, encryption, and traffic handling.
  • This post is not meant as a statement, a call to action, or a recommendation that everyone should run a proxy server. It is simply a technical walkthrough created while researching and experimenting with these tools.
  • It's also important to remember that a proxy does not make you anonymous. Even when using encrypted tunnels, there are still many ways activity can be traced or correlated. Do not assume that a proxy removes accountability or makes actions untraceable.
  • I do not encourage the use of these technologies for illegal, abusive, or shady activities. They are legitimate networking tools that can be used responsibly for privacy, research, development, and infrastructure purposes.
  • Finally, be aware that in some countries the use of certain proxy technologies, or even access to information about them, may be restricted or regulated. Laws and policies vary widely depending on location. Make sure you understand the legal environment where you operate and the risks involved.

So use this information responsibly, Luke. Always stay aware of the broader context around these tools.

Prerequisites

Let's also make sure you have a few basic things ready.

  • First, you need a server. In most cases this will be a VPS hosted somewhere like Hetzner, DigitalOcean, Vultr, OVH, or any other provider you prefer. The exact provider doesn't matter much for this setup as long as you have a public IP address and root access.
  • In this guide we assume the server is running Ubuntu, since it is one of the most common Linux distributions and widely supported by hosting providers. The commands shown here are written for Ubuntu, so if you use another distribution some steps may look slightly different.
  • You'll also need access to a terminal on your own computer. On Linux and macOS this is already built in. On Windows you can use tools like Windows Terminal, PowerShell, or SSH clients such as PuTTY.
  • Finally, it helps to have a basic understanding of Linux commands. Nothing advanced is required, but you should be comfortable with things like connecting over SSH, editing files with a terminal editor, and running commands with sudo.

If you have those pieces ready, you are good to go!

Prepare the server

Before we install anything, let's give the server a quick cleanup and update. This part is not strictly mandatory, but honestly it's just good practice. Starting from a fresh and fully updated system reduces the chances of running into random dependency issues later.

If this is a brand new VPS, just run the following commands and let the system catch up:

sudo apt update  
sudo apt upgrade -y  
sudo apt full-upgrade -y  
sudo apt autoremove -y  
sudo apt autoclean

Quick rundown of what happens here:

  • apt update refreshes the package list from the repositories.
  • apt upgrade installs normal package updates.
  • apt full-upgrade handles upgrades that involve dependency changes.
  • apt autoremove removes packages that are no longer needed.
  • apt autoclean clears out old cached package files.

The whole thing usually takes a minute or two depending on the server. Grab a sip of coffee, let it finish, and then we can move on.

Lock down SSH and switch to key access

Most fresh servers come with only one way to log in: the root account and a password. That works, but it's not something you want to keep around for long. The first thing we will do is switch to key-based authentication, disable root login, and only allow a dedicated user to connect.

Tip: never work as root unless there's a very specific reason.

The idea is simple: you generate an SSH keypair on your own computer (not on the server!). Then:

  • The private key stays on the local machine.
  • The public key goes on the server.
  • The server will only allow logins that have the correct key.

We'll use an Ed25519 key based on elliptic curve cryptography; it's fast, modern, and widely supported. So, generate the key on your own machine:

ssh-keygen -t ed25519 -C "server-access"

This creates a keypair in the local ~/.ssh directory. Again, the private key stays on your machine. Do not share it with anyone. The public key (usually id_ed25519.pub) is what we'll copy to the server.

Now let's prepare a new user on the server and give it sudo access. Do this while logged in as root:

adduser kruk  
usermod -aG sudo kruk

Next create the SSH directory for that user.

mkdir -p /home/kruk/.ssh  
chmod 700 /home/kruk/.ssh  
vim /home/kruk/.ssh/authorized_keys

Paste your public key from your local machine into this authorized_keys file and save it.

Then fix permissions and ownership:

chmod 600 /home/kruk/.ssh/authorized_keys  
chown -R kruk:kruk /home/kruk/.ssh

Now we tighten the SSH configuration:

sudo vim /etc/ssh/sshd_config

Update or add the following lines:

KbdInteractiveAuthentication no  
UsePAM yes  
X11Forwarding no  

PermitRootLogin no  
PasswordAuthentication no  
PubkeyAuthentication yes  
AllowUsers kruk

This does a few important things:

  • disables password authentication
  • disables root login over SSH
  • allows only the kruk user to connect
  • requires a valid SSH key

After saving the file, restart SSH:

sudo systemctl restart ssh

Very important: before closing your current root session, open a separate terminal and try logging in with the new user and its SSH key. If something goes wrong and the root session is closed too early, you could lock yourself out of the server.

Once you confirm the new login works, it should be safe to close the root session.

Setup a basic firewall with ufw

Now let's add a simple firewall. Linux already has powerful firewall tools under the hood, but working with them directly can be a bit annoying. Since the goal here is to keep things simple, we'll utilize something with a very fitting title: ufw, short for Uncomplicated Firewall. Pretty convenient name, and for once it's not lying.

The idea is simple: block everything coming into the server except what's explicitly allowed. For now, only SSH stays open so access to the machine isn't lost.

First install ufw:

sudo apt install ufw -y

If this is a brand new server, you can safely reset all firewall rules to start clean. If you are working on an existing server with services already running, be careful with ufw reset because it wipes all current rules.

sudo ufw reset

Now we define the default behavior:

sudo ufw default deny incoming  
sudo ufw default allow outgoing

This means:

  • incoming connections are blocked unless we allow them
  • outgoing connections from the server are allowed

Next, allow SSH access with rate limiting, which helps reduce brute-force attacks:

sudo ufw limit ssh

Now enable the firewall:

sudo ufw enable

You may get a warning about SSH connections: that's normal. Since we already allowed SSH above, it's safe to continue.

Finally check the firewall status:

sudo ufw status verbose

At this point the server accepts SSH connections and blocks everything else. Later on, only the specific ports needed for the proxy services will be opened.

Add brute-force protection with fail2ban

Even with key-based SSH access, it is still a good idea to block bots that keep hammering your server with login attempts. That is where fail2ban comes in.

Fail2ban watches log files and looks for suspicious patterns. If an IP keeps failing authentication, it gets banned automatically for a period of time. This cuts down noise from scanners and brute-force scripts.

First install it:

sudo apt install fail2ban -y

Enable and start the service:

sudo systemctl enable fail2ban  
sudo systemctl start fail2ban

Now we create a simple configuration file. Instead of editing the main config, we create our own override file:

sudo vim /etc/fail2ban/jail.local

Add the following configuration:

[DEFAULT]  
bantime = 1h  
findtime = 10m  
maxretry = 5  
backend = systemd  

[sshd]  
enabled = true  
port = ssh  
logpath = %(sshd_log)s  

Quick explanation of the key settings:

  • bantime is how long an IP stays banned.
  • findtime is the window where failures are counted.
  • maxretry is how many failed attempts are allowed before a ban.
  • backend = systemd tells fail2ban to read logs from systemd journal.

In this setup, if someone fails SSH login 5 times within 10 minutes, they get banned for 1 hour.

Save the file and restart fail2ban:

sudo systemctl restart fail2ban

Now check that the service is running:

sudo fail2ban-client status

You should see sshd listed as an active jail. To inspect it specifically:

sudo fail2ban-client status sshd

You can also verify that fail2ban correctly detects failed SSH logins. This command tests the SSH filter against your logs:

sudo fail2ban-regex /var/log/auth.log /etc/fail2ban/filter.d/sshd.conf

To quickly see failed SSH attempts:

sudo grep "Failed password" /var/log/auth.log

At this point the server automatically blocks IPs that try to brute-force SSH.

Run your own local DNS resolver with unbound

Another relatively small but useful step is running a local DNS resolver on the server. This part is optional, but it gives more control over how domain lookups are handled.

Normally a VPS uses DNS from the hosting provider or from a public resolver like Google or Cloudflare. That works fine, but it also means DNS queries go through somebody else's resolver, so part of the lookup process depends on an external service.

With a local resolver like unbound, the server resolves domains itself by talking directly to the root DNS servers and following the normal DNS chain from there. It can cache results locally, validate DNSSEC, and reduce dependence on the VPS provider DNS. In simple terms, the server becomes its own DNS resolver.

That is useful for a few reasons:

  • better control over DNS resolution
  • local caching, which can speed up repeated lookups
  • less dependency on the hosting provider DNS
  • optional DNSSEC validation for extra integrity

Is this DoT?

Just to keep it clear, this isn't DNS-over-TLS. In our setup unbound does classic recursive DNS resolution, which means it talks to the DNS hierarchy itself instead of forwarding queries to Google, Cloudflare, or another upstream resolver.

Yes, you can do DNS-over-TLS if you want, but then the usual practical setup is different: unbound forwards queries to an external DNS provider over an encrypted channel. That gives you encrypted DNS transport, but it also means you are trusting a third-party resolver again.

For this guide we keep it independent: local unbound, local cache, and direct recursive resolution.

Installing and configuring unbound

Now let's install curl and unbound:

sudo apt install curl unbound -y

Check that the service is installed:

sudo systemctl status unbound

Next download the root DNS hints file. This tells unbound where the root DNS servers are.

sudo curl -o /var/lib/unbound/root.hints https://www.internic.net/domain/named.root

Now create a resolver configuration file:

sudo vim /etc/unbound/unbound.conf.d/resolver.conf

Add the following configuration:

server:

    interface: 127.0.0.1
    port: 53

    do-ip4: yes
    do-ip6: yes
    do-udp: yes
    do-tcp: yes

    root-hints: "/var/lib/unbound/root.hints"

    harden-glue: yes
    harden-dnssec-stripped: yes
    use-caps-for-id: yes

    edns-buffer-size: 1232

    prefetch: yes
    qname-minimisation: yes

    hide-identity: yes
    hide-version: yes

    access-control: 127.0.0.0/8 allow
    access-control: ::1 allow

    cache-min-ttl: 3600
    cache-max-ttl: 86400

    prefetch-key: yes

    msg-cache-size: 128m
    rrset-cache-size: 256m

    num-threads: 2

This configuration does a few things:

  • listens only on 127.0.0.1 so it is not exposed to the internet
  • resolves domains starting from the DNS root servers
  • enables DNS hardening and DNSSEC protections
  • caches responses to speed up repeated lookups (you might want to experiment with these further)
  • hides server identity details
  • sets number of threads (use with caution, remove if unsure)

Before restarting the service, check the configuration:

sudo unbound-checkconf

If there are no errors, restart unbound:

sudo systemctl restart unbound

Now test the resolver:

dig google.com @127.0.0.1

You should see a normal DNS response. You can also confirm that unbound is listening on port 53:

ss -tulpn | grep :53

Configuring resolvers

Next we tell the system to use this resolver locally. Edit the systemd resolver configuration:

sudo vim /etc/systemd/resolved.conf

Update or add these values:

DNS=127.0.0.1  
FallbackDNS=  
DNSSEC=allow-downgrade  
DNSStubListener=no
DNSOverTLS=no  

This setup tells the system to use the local Unbound resolver and disables the systemd-resolved stub listener on 127.0.0.53.

Restart the resolver service:

sudo systemctl restart systemd-resolved

Check the resolver status:

resolvectl status

Also confirm the resolver configuration:

sudo cat /etc/resolv.conf

On some systems it may still point somewhere else. If needed, recreate the proper symlink:

sudo rm /etc/resolv.conf  
sudo ln -s /run/systemd/resolve/stub-resolv.conf /etc/resolv.conf

At this point the server is configured to send DNS queries directly to the local Unbound resolver on 127.0.0.1. Unbound handles recursive resolution and local caching. If you enabled DNSSEC validation in Unbound, validation also happens there.

Watch out for DNS leaks on the client side

One important thing to keep in mind: even if your proxy server is configured correctly, the client device can still leak DNS requests outside the tunnel.

This matters because some networks and providers don't just block traffic itself: they also watch DNS very closely. If your device keeps resolving domains through the local ISP resolver instead of through the proxy path, that can break access, expose what you are trying to open, or just make the whole setup behave in a weird and inconsistent way.

So when testing the client, do not only check whether the proxy connects. Also check how DNS is handled on that device. What you generally want is this:

  • the client should send traffic through the proxy
  • DNS lookups shouldn't go directly to the local provider unless you explicitly want that
  • there should be no split behavior where some domains go through the tunnel and others leak outside

This is especially important on restrictive networks where DNS queries themselves may be filtered, logged, or tampered with.

In practice, this means you should pay attention to the DNS settings inside the client app and on the operating system itself. Some clients can proxy DNS correctly on their own; some do it only partially; some leave DNS to the system, which can lead to leaks if the system resolver still points to the local provider.

If you need extra control on the client side, tools like dnscrypt-proxy can help route DNS more safely. On macOS, some people also use services like NextDNS or DNS Cloak for encrypted client-side DNS. The exact choice depends on the platform and the threat model, but the main point is simple: make sure your DNS path matches the way you expect the proxy to work.

A proxy that connects fine but leaks DNS is only doing half the job.

Enable BBR congestion control

Before we move on to the proxy stack, it's worth enabling a modern TCP congestion control algorithm. By default many Linux systems still use older algorithms like cubic. They work fine, but newer options can improve throughput and latency on long-distance connections.

One of the most widely used options today is BBR (Bottleneck Bandwidth and Round-trip propagation time). It was developed by Google and focuses on keeping the connection fast without flooding the network with unnecessary packets. In practice it often improves performance for servers that handle lots of connections or traffic across different regions.

Some kernels already include BBR but do not enable it automatically. First check what congestion control algorithms are available on your system:

sysctl net.ipv4.tcp_available_congestion_control

If bbr appears in the list, you can enable it directly. If not, load the kernel module manually.

sudo modprobe tcp_bbr

Verify that the module is loaded:

lsmod | grep bbr

Now we configure the system to use BBR by default. Open the sysctl configuration file:

sudo vim /etc/sysctl.conf

Add the following lines at the end of the file:

net.core.default_qdisc = fq  
net.ipv4.tcp_congestion_control = bbr

The fq queue discipline works together with BBR and helps manage packet scheduling more efficiently.

Apply the configuration:

sudo sysctl -p

Finally confirm that BBR is now active:

sysctl net.ipv4.tcp_congestion_control

If everything is set correctly, the output should show:

bbr

The server is now using BBR for TCP connections, which usually results in better throughput and more stable latency under load.

Install sing-box

Now it is time to install the main tool behind the whole setup: sing-box.

Sing-box is a modern networking toolbox that supports multiple proxy protocols in one service. Instead of stitching together separate tools for SOCKS, VLESS, Hysteria2, DNS, and routing, a lot of that can live in one place with a single configuration.

The official project currently provides an APT repository for Debian and Ubuntu, so it makes sense to use that instead of pulling random builds from somewhere else.

First create a directory for APT keyrings, then download the repository signing key:

sudo mkdir -p /etc/apt/keyrings
sudo curl -fsSL https://sing-box.app/gpg.key -o /etc/apt/keyrings/sagernet.asc
sudo chmod a+r /etc/apt/keyrings/sagernet.asc

Now add the repository itself:

echo '
Types: deb
URIs: https://deb.sagernet.org/
Suites: *
Components: *
Enabled: yes
Signed-By: /etc/apt/keyrings/sagernet.asc
' | sudo tee /etc/apt/sources.list.d/sagernet.sources

Refresh the package list so the system sees the new repository:

sudo apt update

Finally install sing-box:

sudo apt install sing-box -y

Once this finishes, sing-box is installed on the system and ready to be configured. In the next steps we will start building the configuration for our proxy protocols.

Prepare a log file for sing-box

Sing-box already logs through systemd by default, so technically you can just use journalctl to read logs. For many setups that is perfectly fine. That said, some people prefer having a dedicated file to monitor logs easily, ship them to a log collector, or just quickly tail the file when debugging.

If you want that setup, we can create a simple log directory owned by the sing-box user:

sudo mkdir -p /var/log/sing-box

Create the log file itself:

sudo touch /var/log/sing-box/sing-box.log

If you decide to follow this path, later it might be wise to configure log rotation.

Now give ownership to the sing-box service user:

sudo chown -R sing-box:sing-box /var/log/sing-box

Set reasonable permissions for the directory:

sudo chmod 750 /var/log/sing-box

And restrict the log file so only the service user can write to it:

sudo chmod 640 /var/log/sing-box/sing-box.log

Prepare TLS certificates for Hysteria2

Sing-box usually runs under its own system user called sing-box. That is good for security because the proxy service doesn't require full root privileges. But it also means we must make sure the service can access the TLS certificate and key it needs.

For the Hysteria2 protocol, TLS is required. That means you need a domain name pointing to your server. Register a domain with any provider you like and create an A record that points to your server IP.

Once the domain resolves correctly, we can issue a certificate using Let's Encrypt.

First install certbot:

sudo apt install certbot -y

Certbot needs temporary access to port 80 to verify the domain. Since we currently block everything except SSH, we briefly open it:

sudo ufw allow 80/tcp

Now issue the certificate (replace the domain with your own if needed):

sudo certbot certonly --standalone -d example.com

Once the certificate is issued, close port 80 again:

sudo ufw delete allow 80/tcp

Now we prepare permissions so the sing-box service can read the certificate safely. Create a group that can access TLS material and add the sing-box user to it:

sudo groupadd ssl-cert  
sudo usermod -aG ssl-cert sing-box

Update ownership and permissions on the Let's Encrypt directory:

sudo chgrp -R ssl-cert /etc/letsencrypt  
sudo chmod -R 750 /etc/letsencrypt

Restrict the private key permissions:

sudo chmod 640 /etc/letsencrypt/live/example.com/privkey.pem

Next we configure automatic renewal hooks. During renewal certbot again needs port 80 temporarily open, so create the pre-renewal hook:

sudo vim /etc/letsencrypt/renewal-hooks/pre/open-port-80.sh

Add:

#!/bin/bash  
ufw allow 80/tcp

Now create the post-renewal hook:

sudo vim /etc/letsencrypt/renewal-hooks/post/close-port-80.sh

Add:

#!/bin/bash  
ufw delete allow 80/tcp  
systemctl reload sing-box

Make both scripts executable:

sudo chmod +x /etc/letsencrypt/renewal-hooks/pre/open-port-80.sh  
sudo chmod +x /etc/letsencrypt/renewal-hooks/post/close-port-80.sh

Test the renewal process:

sudo certbot renew --dry-run

If the sing-box service is not yet running, you might see an error at this step. It's fine, and you can execute this dry run a bit later when the service is actually started.

Now we create a cleaner location where sing-box will reference the certificates:

sudo mkdir -p /etc/sing-box/certs

Create symbolic links pointing to the real certificate files:

sudo ln -s /etc/letsencrypt/live/example.com/fullchain.pem /etc/sing-box/certs/cert.pem  
sudo ln -s /etc/letsencrypt/live/example.com/privkey.pem /etc/sing-box/certs/key.pem

Set permissions so only the appropriate users can access this directory:

sudo chown -R root:ssl-cert /etc/sing-box  
sudo chmod -R 750 /etc/sing-box

Finally we ensure the sing-box user can always read the certificate directories, including future renewals. Install ACL support:

sudo apt install acl -y

Grant read access to the sing-box user:

sudo setfacl -R -m u:sing-box:rx /etc/letsencrypt

Make sure new files inherit the same permissions:

sudo setfacl -R -d -m u:sing-box:rx /etc/letsencrypt

You can verify the permissions with:

sudo getfacl /etc/letsencrypt/live

At this point your server has a valid TLS certificate, automatic renewal configured, and sing-box can safely access the required files.

Setup a SOCKS proxy with sing-box

Before we add more advanced protocols, it is useful to expose a simple SOCKS proxy. Many apps support SOCKS out of the box, including Telegram, browsers, and various dev tools. This gives you a quick way to route traffic through your server.

Tip: Don't blindly use random public or unknown proxy lists for anything sensitive. The operator behind that proxy can log traffic metadata, inspect unencrypted requests, inject junk, steal credentials in badly configured setups, or just sell the data later. Free proxies are often unreliable at best and outright hostile at worst.

First generate a strong password that will be used for authentication:

openssl rand -hex 32

This produces a long random string. Use it as the proxy password instead of something human-readable.

Now create or update the sing-box configuration file:

sudo vim /etc/sing-box/config.json

Example configuration:

{
  "log": {
    "level": "info",
    "output": "/var/log/sing-box/sing-box.log"
  },
  "dns": {
    "servers": [
      {
        "type": "tcp",
        "tag": "dns_unbound",
        "server": "127.0.0.1",
        "server_port": 53
      }
    ],
    "final": "dns_unbound",
    "strategy": "prefer_ipv4",
    "cache_capacity": 4096
  },
  "route": {
    "final": "direct",
    "default_domain_resolver": {
      "server": "dns_unbound",
      "strategy": "prefer_ipv4"
    }
  },
  "inbounds": [
    {
      "type": "socks",
      "tag": "tg-socks",
      "listen": "SERVER_IP",
      "listen_port": 1081,
      "users": [
        {
          "username": "utg",
          "password": "your_generated_password_here"
        }
      ]
    }
  ],
  "outbounds": [
    {
      "type": "direct",
      "tag": "direct",
      "domain_resolver": {
        "server": "dns_unbound",
        "strategy": "prefer_ipv4"
      }
    }
  ]
}

Let's quickly walk through the important parts:

  • log sends sing-box logs to the file we created earlier.
  • dns tells sing-box to use the local unbound resolver running on 127.0.0.1. That way all domain lookups go through the local DNS resolver instead of external providers.
  • route simply forwards traffic directly to the internet. In other words, the server acts as a normal gateway for proxy clients.
  • inbounds defines services that accept incoming connections. Here we create a SOCKS proxy:
    • type: socks enables a SOCKS5 proxy
    • listen is your server IP
    • listen_port is the port clients will connect to
    • users defines authentication credentials
  • outbounds describes how traffic leaves the server. In this case it goes directly to the destination.

Once the config file is saved, open the SOCKS port in the firewall:

sudo ufw allow 1081/tcp

Before starting the service, verify the configuration:

sudo sing-box check -c /etc/sing-box/config.json

If there are no errors, start the service:

sudo systemctl start sing-box  
sudo systemctl enable sing-box  
sudo systemctl status sing-box

You can restart it later with:

sudo systemctl restart sing-box  

Confirm that sing-box is listening on the SOCKS port:

ss -tulpn | grep 1081

If you want to watch logs live while testing the proxy:

sudo journalctl -u sing-box -f

At this point you have a working SOCKS proxy that can be used in apps like Telegram or in your browser proxy settings.

Using the SOCKS proxy in Telegram

One nice thing about Telegram is that it supports SOCKS proxies natively and even allows you to add them with a simple link.

Using the configuration from the previous step, a Telegram proxy link would look like this:

https://t.me/socks?server=SERVER_IP&port=1081&user=utg&pass=YOUR_GENERATED_PASSWORD

If someone opens that link on a device with Telegram installed, the app will offer to add the proxy automatically. After that, Telegram traffic will go through your server. Of course, you should only share this link with people you trust since it contains the proxy credentials.

If you want to verify that clients are connecting successfully, you can watch the sing-box logs in real time:

sudo tail -f /var/log/sing-box/sing-box.log

When someone connects through the proxy, you should see connection events appearing in the log. This is also helpful for debugging if something doesn't work as expected.

Protect the SOCKS proxy with fail2ban

Since the SOCKS proxy is exposed to the internet, bots may eventually start scanning it or trying random credentials. Even with a strong password, it is a good idea to block repeated authentication failures automatically. We can extend our existing fail2ban setup to monitor sing-box logs and ban IPs that keep failing SOCKS authentication.

First create a custom fail2ban filter for sing-box:

sudo vim /etc/fail2ban/filter.d/singbox-socks.conf

Add the following:

[Definition]  
failregex = ^ERROR .*inbound/socks\[tg-socks\]: process connection from <HOST>:\d+: socks5: authentication failed\b.*$  
ignoreregex =

This tells fail2ban how to recognize failed SOCKS login attempts in the sing-box log. Before enabling the jail, it is a good idea to test the filter against your logs:

sudo fail2ban-regex /var/log/sing-box/sing-box.log /etc/fail2ban/filter.d/singbox-socks.conf

If the filter works correctly, fail2ban will detect matching lines in the log output.

Now add a new jail for the SOCKS proxy:

sudo vim /etc/fail2ban/jail.local

Add this section:

[singbox-socks]

enabled  = true  
filter   = singbox-socks  
logpath  = /var/log/sing-box/sing-box.log  
backend  = auto  

maxretry = 5  
findtime = 10m  
bantime  = 24h  

port     = 1081  
action   = ufw

What this configuration does:

  • watches the sing-box log file
  • detects failed SOCKS authentication attempts
  • bans the IP after 5 failures within 10 minutes
  • blocks the IP for 24 hours
  • uses ufw to enforce the ban

Restart fail2ban to activate the new jail:

sudo systemctl restart fail2ban

Check the status of fail2ban:

sudo fail2ban-client status

And inspect the SOCKS jail specifically:

sudo fail2ban-client status singbox-socks

Add VLESS + Reality proxy

Now let's add a more advanced protocol: VLESS with Reality. VLESS is a lightweight proxy protocol commonly used in the Xray ecosystem. On its own, it's already quite efficient, but when combined with Reality it becomes much harder to detect. Reality works by mimicking a normal TLS connection to a real website. To outside observers the traffic looks like a regular encrypted HTTPS connection.

This approach is useful in restrictive networks where traditional proxies might be blocked. That said, network filtering rules vary by country and provider, so availability may differ depending on where the server or client is located.

Before adding the configuration, we need a few values. Generate a UUID for client authentication:

uuidgen

Next, generate the Reality keypair:

sing-box generate reality-keypair

This will output a public key and a private key. The private key stays on the server, while the public key is used in client configuration.

Now generate a short identifier used during the handshake:

openssl rand -hex 4

Next open the port that will be used for the VLESS service:

sudo ufw allow 10443/tcp

Update the sing-box configuration file:

sudo vim /etc/sing-box/config.json

Example configuration with both the existing SOCKS proxy and the new VLESS inbound:

{
  "log": {
    "level": "info",
    "output": "/var/log/sing-box/sing-box.log"
  },
  "dns": {
    "servers": [
      {
        "type": "tcp",
        "tag": "dns_unbound",
        "server": "127.0.0.1",
        "server_port": 53
      }
    ],
    "final": "dns_unbound",
    "strategy": "prefer_ipv4",
    "cache_capacity": 4096
  },
  "route": {
    "final": "direct",
    "default_domain_resolver": {
      "server": "dns_unbound",
      "strategy": "prefer_ipv4"
    }
  },
  "inbounds": [
    {
      "type": "socks",
      "tag": "tg-socks",
      "listen": "SERVER_IP",
      "listen_port": 1081,
      "users": [
        {
          "username": "utg",
          "password": "your_socks_password"
        }
      ]
    },
    {
      "type": "vless",
      "tag": "vless-reality",
      "listen": "SERVER_IP",
      "listen_port": 10443,
      "users": [
        {
          "uuid": "your_generated_uuid",
          "flow": "xtls-rprx-vision"
        }
      ],
      "tls": {
        "enabled": true,
        "server_name": "example.com",
        "min_version": "1.3",
        "reality": {
          "enabled": true,
          "handshake": {
            "server": "example.com",
            "server_port": 443
          },
          "private_key": "your_private_key",
          "short_id": [
            "your_short_id"
          ]
        }
      }
    }
  ],
  "outbounds": [
    {
      "type": "direct",
      "tag": "direct",
      "domain_resolver": {
        "server": "dns_unbound",
        "strategy": "prefer_ipv4"
      }
    }
  ]
}

Here are the key parts of this configuration:

  • users contains the UUID used by clients to authenticate.
  • flow enables optimized TLS transport used by modern VLESS clients.
  • Inside the tls section, server_name defines the domain that the connection pretends to talk to. It should be a valid domain with a working TLS site.
  • The reality block handles the camouflage layer:
    • private_key is generated earlier with the reality-keypair command
    • short_id is the random identifier generated with openssl
    • handshake.server is the domain that will appear in the TLS handshake

Once the configuration is saved, validate it:

sudo sing-box check -c /etc/sing-box/config.json

If the configuration is valid, restart the service:

sudo systemctl restart sing-box  
sudo systemctl status sing-box

Verify that the service is listening on the new port:

ss -tulpn | grep 10443

If you want to watch connection activity or debug issues, follow the logs:

sudo journalctl -u sing-box -f

At this point the server exposes both a SOCKS proxy and a VLESS + Reality endpoint.

Connect a client to VLESS + Reality

Once the server side is running, you need a client app that supports VLESS + Reality.

A few common options:

  • Windows: v2rayN
  • Android: v2rayNG
  • macOS / iPhone: clients like Streisand

Most of these apps let you import a connection using a single URL, which is the easiest way to get started.

Example client URL:

vless://UUID@SERVER_IP:10443?encryption=none&flow=xtls-rprx-vision&security=reality&sni=example.com&fp=chrome&pbk=PUBLIC_KEY&sid=SHORT_ID&type=tcp&headerType=none#vless-10443

A few important fields in this link:

  • the UUID matches the user you created on the server
  • the IP and port point to your server
  • security=reality enables Reality mode
  • sni=example.com must match the domain used in the server config
  • pbk= is the public key generated from sing-box generate reality-keypair
  • sid= is the short ID from the server config
  • fp=chrome sets a browser-like fingerprint that many clients support

Import the link into your client, connect, and test access.

If the client does not connect right away, the first place to look is the sing-box log:

sudo tail -f /var/log/sing-box/sing-box.log

That will usually show whether the client reached the server, whether authentication worked, and whether the Reality handshake completed properly.

You also might be interested to learn How to get someone's IP address safely and legally.

Protect VLESS + Reality with fail2ban

Just like the SOCKS proxy, the VLESS endpoint is exposed to the internet. That means scanners or random clients may try connecting with invalid parameters. Reality will reject them, but it is still a good idea to block repeated failures automatically.

We can extend fail2ban again and watch for failed Reality handshakes in the sing-box logs. First create a filter that detects invalid Reality connections:

sudo vim /etc/fail2ban/filter.d/singbox-vless-reality.conf

Add the following:

[Definition]  
failregex = ^ERROR .*inbound/vless\[vless-reality\]: process connection from <HOST>:\d+: TLS handshake: REALITY: processed invalid connection$  
ignoreregex =

This pattern matches log entries where sing-box rejects a connection because the Reality handshake is invalid.

Before enabling the jail, test the filter against the log file.

sudo fail2ban-regex /var/log/sing-box/sing-box.log /etc/fail2ban/filter.d/singbox-vless-reality.conf --print-all-matched

If the filter is correct, fail2ban will show matched lines from the log. That said, it's also completely normal if nothing matches yet. On a fresh server there may simply be no failed Reality handshakes in the log at this point, because nobody has tried poking that port yet.

Now create a new jail:

sudo vim /etc/fail2ban/jail.local

Add this section:

[singbox-vless-reality]

enabled  = true  
filter   = singbox-vless-reality  
logpath  = /var/log/sing-box/sing-box.log  
backend  = auto  

maxretry = 5  
findtime = 10m  
bantime  = 1h  

port     = 10443  
action   = ufw

This configuration means:

  • fail2ban monitors the sing-box log file
  • if an IP triggers 5 invalid Reality handshakes within 10 minutes
  • it gets blocked for 1 hour
  • the ban is applied using ufw

Restart fail2ban so the new jail becomes active:

sudo systemctl restart fail2ban

Check that everything is running:

sudo fail2ban-client status

And verify the Reality jail specifically:

sudo fail2ban-client status singbox-vless-reality

Now repeated invalid Reality connections will automatically get blocked by the firewall.

Add Hysteria2 proxy

Now let's add Hysteria2. The main idea behind Hysteria2 is pretty simple: it uses QUIC over UDP instead of classic TCP. That often makes it feel faster and smoother on unstable networks, mobile connections, or routes with packet loss. In real life that can mean better speed, lower latency spikes, and less pain when the network is being weird.

It's also a nice complement to VLESS + Reality. Some networks behave better with one protocol than the other, so having both gives you options.

For Hysteria2, we need a password for the user. Generate a strong one:

openssl rand -hex 32

Since Hysteria2 uses UDP, we need to open the UDP port in the firewall:

sudo ufw allow 443/udp

Now update the sing-box config and add a new Hysteria2 inbound. Here's an example config:

{
  "log": {
    // ...
  },
  "dns": {
    // ...
  },
  "route": {
    // ...
  },
  "inbounds": [
    {
      "type": "hysteria2",
      "tag": "hy2-in",
      "listen": "SERVER_IP",
      "listen_port": 443,
      "up_mbps": 200,
      "down_mbps": 200,
      "users": [
        {
          "name": "main",
          "password": "your_hysteria2_password"
        }
      ],
      "tls": {
        "enabled": true,
        "server_name": "example.com",
        "min_version": "1.3",
        "certificate_path": "/etc/sing-box/certs/cert.pem",
        "key_path": "/etc/sing-box/certs/key.pem"
      },
      "masquerade": {
        "type": "string",
        "status_code": 200,
        "headers": {
          "content-type": "text/plain; charset=utf-8"
        },
        "content": "ok"
      }
    }
  ],
  "outbounds": [
    // ...
  ]
}

A few important parts here.

  • type is hysteria2, which tells sing-box to accept Hysteria2 client connections.
  • The service listens on UDP port 443. That is a common and practical choice because it looks normal and is often less suspicious than random high ports.
  • up_mbps and down_mbps values define the bandwidth hints for the protocol. They don't magically create speed, but they help shape the connection behavior. Set them roughly in line with what your server and network can actually handle.
  • users section defines the authentication password clients will use.
  • tls block points to the certificate and key we prepared earlier. Unlike Reality, Hysteria2 uses a normal TLS certificate for your domain.
  • masquerade section defines what sing-box returns if someone hits the service in a way that does not look like a valid Hysteria2 client. Here it simply replies with a plain ok response, which is a clean and harmless fallback.

Once the config is saved, validate it:

sudo sing-box check -c /etc/sing-box/config.json

If the check passes, restart sing-box:

sudo systemctl restart sing-box  
sudo systemctl status sing-box

Now confirm that the server is listening on UDP 443:

ss -tulpn | grep 443

And if you want to watch what happens while testing clients:

sudo journalctl -u sing-box -f

At this point your server should expose three different entry points: SOCKS, VLESS + Reality, and Hysteria2.

Add obfuscation to Hysteria2

If you need Hysteria2 to blend in better, you may want to enable obfuscation. Plain Hysteria2 is fast and solid, but by itself it can still be recognizable to systems that look closely at traffic patterns. That may not matter on every network, but on more restrictive ones it can make a real difference.

The basic idea of obfs is the following: it adds an extra disguise layer so the traffic is harder to fingerprint. It doesn't replace TLS and it doesn't magically make traffic invisible, but it can make detection more difficult.

First generate a random password for the obfuscation layer:

openssl rand -hex 16

Now add an obfs block to the Hysteria2 inbound in your sing-box config. It goes alongside the TLS settings.

Example:

{
  "type": "hysteria2",
  "users": [
    {
      "name": "main",
      "password": "your_hysteria2_password"
    }
  ],
  // ... other config ...
  "obfs": {
    "type": "salamander",
    "password": "your_generated_obfs_password"
  }
}

The important parts here are:

  • type: salamander enables the Salamander obfuscation mode supported by Hysteria2
  • password is the shared secret that must match on both the server and the client

After you save the config, validate it and restart sing-box:

sudo sing-box check -c /etc/sing-box/config.json  
sudo systemctl restart sing-box

If the client also uses the same obfs settings, Hysteria2 will now connect with that extra disguise layer on top.

Connect a client to Hysteria2

Just like with VLESS, most modern proxy clients allow you to import a Hysteria2 connection using a single URL. This makes it very easy to share or add configurations.

Many of the same apps support Hysteria2 as well:

  • Windows: v2rayN
  • Android: v2rayNG
  • macOS / iPhone: clients like Streisand

Example connection URL:

hysteria2://PASSWORD@example.com:443?sni=example.com&alpn=h3&insecure=0&allowInsecure=0&obfs=salamander&obfs-password=0ef4ce224ca4dfc6c4bbb2de3a5ebcc0#hysteria2

Here are the important parts in that link:

  • the password before @ is the Hysteria2 user password from the server config
  • example.com:443 points to your server and UDP port
  • sni=example.com must match the domain used in the TLS configuration
  • alpn=h3 tells the client to use HTTP/3 over QUIC
  • obfs=salamander enables the obfuscation mode
  • obfs-password must match the password defined in the server config
  • settings like allowInsecure and insecure might be recognized by some clients, but these are not mandatory

Once you import this link into your client and connect, traffic will be routed through the Hysteria2 server. If something doesn't work, the fastest way to debug is still watching the sing-box log while connecting:

sudo tail -f /var/log/sing-box/sing-box.log

You should see connection attempts, authentication results, and any TLS or protocol errors that might help identify the issue.

Do you need fail2ban for Hysteria2?

In most cases you don't really need fail2ban for Hysteria2.

Unlike SOCKS or some other proxies, Hysteria2 doesn't expose a simple authentication exchange that bots can repeatedly brute-force in the same way. The protocol runs over QUIC with TLS and a shared password, so random scanners usually fail very early in the connection process.

In practice this means:

  • random internet scanners rarely reach the authentication stage
  • failed attempts usually do not generate useful patterns for fail2ban
  • aggressive blocking often provides little real benefit

Because of that, many setups simply run Hysteria2 without any fail2ban rules at all.

If you want additional protection, the strong password, TLS, and optional obfuscation layer already provide a solid baseline. Combined with the firewall and the other protections we configured earlier, this is usually more than enough for a personal proxy server.

Need proxies for scraping at scale?

Running personal proxies is cool and very useful for learning, testing, and having full control over the setup. But for real production scraping, things get messy pretty fast. One server is usually not enough, one proxy is usually not enough, and sooner or later the whole thing turns into proxy rotation, bans, browser rendering, fingerprint issues, retries, and a lot of random pain.

That is where a service like ScrapingBee can make a lot more sense.

Instead of building and maintaining the whole scraping stack by hand, ScrapingBee handles the heavy lifting and gives access to proxy infrastructure out of the box. That includes premium proxies and even stealth proxies for especially annoying websites that love blocking requests.

It also does more than just send requests through proxies. ScrapingBee can render pages like a real browser, which is a big deal for modern JavaScript-heavy sites. On top of that, it offers AI extraction features and even a no-code solution if there is no desire to wire everything together manually.

If the goal is reliable scraping without spending half of life fighting anti-bot systems, it is worth checking out.

So, sign up today! No credit card is needed, and you get 1,000 free scraping credits as a welcome bonus.

Final thoughts

At this point you have a pretty flexible proxy setup running on your server. SOCKS works great for quick integrations like Telegram or browsers, VLESS + Reality provides a stealthy TCP option, and Hysteria2 gives you a fast UDP-based transport that often performs well on unstable networks.

A small improvement you might consider is running a dummy website on your server. For example, you could host a simple static site on TCP port 443 using nginx or another web server. That will not interfere with Hysteria2 since it uses UDP on port 443, and it gives your domain a normal-looking HTTPS presence if someone opens it in a browser.

You may also want to experiment with other protocols supported by sing-box. One common option is Trojan, which behaves like a normal HTTPS connection and can be useful in some environments. Sing-box supports quite a few transports, so once the base system is in place it is easy to extend.

A few final tips that are worth keeping in mind:

  • keep your system updated regularly
  • rotate passwords or keys if you ever share them
  • watch the logs occasionally to understand how your server is being used
  • do not expose services you do not actually need

Most importantly, you now understand the building blocks behind the setup instead of just running a script that hides everything. That makes troubleshooting, expanding, and maintaining the system much easier later.

Nice work: you now have your own multi-protocol proxy server running on Linux. Thanks for staying with me, and until next time.

How to build a private proxy server: FAQ

Do I really need all these protocols?

Nope. One is enough to get started. The rest are just options for different networks and situations.

Is this setup anonymous?

No. It adds a layer, not invisibility. Logs, fingerprints, and other signals still exist.

Why not just use a VPN?

You can. This is more about control and understanding how things work under the hood.

Can I get blocked anyway?

Yep. Especially on strict networks. That's why having multiple protocols helps.

Do I need a domain for everything?

Only for things like TLS (Hysteria2). SOCKS can work with just an IP.

What if I want to use this for scraping?

For small experiments — sure. For real production scraping, better use something like ScrapingBee. Managing proxy pools yourself gets messy fast.

Is this safe to run long-term?

If you keep it updated, use strong creds, and watch logs — yeah, for personal use it's fine.

image description
Ilya Krukowski

Ilya is an IT tutor and author, web developer, and ex-Microsoft/Cisco specialist. His primary programming languages are Ruby, JavaScript, Python, and Elixir. He enjoys coding, teaching people and learning new things. In his free time he writes educational posts, participates in OpenSource projects, tweets, goes in for sports and plays music.