# OxidProxy — deployment by an AI agent This file is written **for an agent**, not for a person. The person hands the agent this instruction and answers its questions; the agent does everything else. **This instruction is written for OxidProxy 2.4.x.** After installation, compare that against what `oxidproxy -V` reports. - **First two digits match** (2.4.0, 2.4.9 — makes no difference) — the configuration format is compatible for certain. Carry on without a second thought. - **Second digit differs** — no guarantee either way. The configuration format *may* have changed, or may not: the second digit also goes up for other reasons. This is not a reason to stop, but it is a reason to check sections 5 and 7 against the documentation and to tell the person that the instruction is older than the software. **Documentation:** https://oxidproxy.com/user_guide.html — every configuration field is explained there. Consult it when something is unclear. But **this instruction takes precedence**: where the two disagree, do as it says here. The documentation is written for a human and covers far more than a first installation needs. --- ## 0. Rules for the agent Read these before you start. 1. **Invent nothing.** If a value is missing, either ask the person or use a default that is stated explicitly below. Never substitute something "approximate" or "similar". 2. **Ask everything at once**, as a single list (section 2), rather than one question per step. 3. **Verify the result of every step.** A command that exited without an error is not the same thing as a step that worked. For each step this instruction states what to check. 4. **Do not print secrets needlessly.** The token and the passwords are shown once, at the very end (section 11). Do not echo them in intermediate messages. 5. **You need root** and **direct SSH access** to the server. 6. If a step fails, do not move on to the next one. See section 12. 7. **A required field cannot be skipped.** If the person did not answer one of the required questions (section 2, items 1–3) or answered vaguely, ask again **in a separate message, about that one field**, and wait for the answer. Do not substitute anything for it and do not move on. 8. **The person may say they will configure the rest themselves.** Then stop asking questions: install the software, fill in only what they gave you, and go to section 11.1 — it says what to hand over. In this case **do not start the service**. --- ## 1. What the customer must have Check before you start, and tell the person if something is missing. | Requirement | How to check | |---|---| | Debian 12 (bookworm), x86_64 architecture | `. /etc/os-release; echo $VERSION_ID $(uname -m)` | | root privileges | `id -u` must return `0` | | Their own IPv6 subnet, routed to this server | ask the person | | An OxidProxy licence key | ask the person | If there is no IPv6 subnet, **you cannot go any further**. The software emits outbound traffic over IPv6 only; without a subnet it is useless. Say so plainly and stop. ### 1.1. The server must be healthy BEFORE you start This is not a formality. If the server is already broken, the software is what will look guilty, and you will have to work it out from scratch. Run four checks and show the person the results: ```bash curl -6 -s --max-time 10 https://ipv6.icanhazip.com/ ; echo "exit code: $?" getent hosts oxidproxy.com timedatectl | grep -E 'System clock|NTP' df -h / | tail -1 ``` - **IPv6 connectivity.** An empty answer means the server has no IPv6 route out of its own. That is not always a dead end — the subnet may have been assigned "on the segment" (see section 12, "Subnet assigned but not routed to the server") — but note the result and tell the person about it right away. The conclusive check comes in section 10. - **DNS.** If names do not resolve, the installer will not download the packages. - **The clock.** A clock that has drifted breaks the licence check: it runs over TLS, and TLS rejects certificates "from the future". Fix it with `timedatectl set-ntp true`. - **Disk space.** The kernel and the package together take about a gigabyte, plus room for the journal. Less than 5 GB free — warn the person. --- ## 2. Questions for the person — ask them in one message Ask everything listed below at once. The parentheses say what to do if the person does not know the answer or has nothing to give. **Required — there are no defaults.** If any of these goes unanswered, ask again separately about that one field and wait (rule 7). 1. **The licence key.** 2. **The server's external IPv4 address** — the one clients will use to reach the proxy. *(If the person does not know: run `curl -4 -s ifconfig.me` and show them the result for confirmation. Do not take it silently: the server may be behind NAT, in which case the auto-detected address will be wrong.)* 3. **IPv6 subnets** in CIDR form, for example `2a01:1234::/32`. **There may be more than one — ask outright: "one or several?"** Collect the whole list; do not stop at the first. Each has its own weight: the larger the number, the more often addresses are drawn from it. *(If no weights were given, set them all the same, 100.)* Every subnet needs **its own line** in `rc.local` (section 6) and **its own entry** in `ipv6_subnets` (section 5.2). Miss one and it simply will not be used, with no error to show for it. **Optional — defaults exist:** 4. **The token for the API and the dashboard.** *(None — generate one yourself, see section 5.)* 5. **DNS servers and weights.** *(None — keep the ones from the template. Do read `/etc/resolv.conf` and show what you found, but only offer to add a **local** resolver: an address in `10.0.0.0/8`, `172.16.0.0/12`, `192.168.0.0/16`, `100.64.0.0/10`, `fc00::/7`, or the gateway itself. That one is almost always faster than a public resolver — offer to add it with weight 1000. If the file points at a public resolver (`8.8.8.8`, `1.1.1.1`, `9.9.9.9` and the like), there is nothing to add: they are already in the template's pool, and a second copy of the same server only skews the weights.)* 6. **The number of accounts.** *(None — set `10000`.)* 7. **Dashboard branding: the wordmark and the site address.** *(None — do not add the `brand` section at all, and our wordmark stays in the dashboard. Ask outright: "Should the dashboard show your name or ours?" The wordmark is split in two, and the second half is painted in the accent colour — for example `Acme` + `Proxy`.)* 8. **Proxy ports.** *(None — keep the template ports: 1080 linux, 1081 android, 1082 windows, 1083 ios, 1084 macos.)* 9. **Whether a MITM port is wanted** — browser fingerprint emulation, not just the OS one. *(No answer — do not enable it. If yes — section 7.)* **Always generated, never asked about:** - `account_seed` — so that logins and passwords survive a restart unchanged; - the service account password (for checkers and monitoring). --- ## 3. Installation ### 3.1. Preparation: update the system and snapshot the settings **Update the system first.** The installer places packages with `dpkg` and resolves dependencies afterwards with `apt-get install -f` — on a server with a stale package index that trips. ```bash apt-get update apt-get -y full-upgrade ``` If the upgrade brought in a kernel newer than the running one (compare `uname -r` against `dpkg -l 'linux-image-*'`), **reboot right now**, before installing. Otherwise you get the reverse: our kernel is installed and the stock one boots. > **Tell the person this, and remember it yourself.** After OxidProxy is > installed, `apt full-upgrade` stops being harmless: if Debian ships a kernel > numbered above ours, GRUB will boot it, and OS fingerprint emulation will > silently stop working. The rule is simple — **after every system upgrade, > check `uname -r` for the `oxidproxy-p0f` suffix**. **Then snapshot what you are about to change.** Not "remember" — save it to disk: a conversation can be cut short or truncated, a file on the server cannot. ```bash BK=/root/oxidproxy-preinstall-$(date +%F-%H%M) mkdir -p "$BK" cp -a /etc/rc.local /etc/default/grub /etc/sysctl.conf "$BK"/ 2>/dev/null cp -a /etc/sysctl.d "$BK"/ 2>/dev/null { uname -r ip -6 addr show scope global ip -6 route show ip -6 route show table local sysctl net.ipv4.ip_local_port_range dpkg -l | grep linux-image } > "$BK"/before.txt 2>&1 echo "$BK" ``` Tell the person the directory you ended up with. If something goes wrong, it shows what the server looked like before we touched it. ### 3.2. Installation One command. It installs the patched kernel, the package itself, and offers the login banner. ```bash curl -fsSL --http1.1 https://oxidproxy.com/downloads/install.sh | bash ``` The installer asks **two questions**: 1. `Install the login banner? (y/N)` — **answer `y`**. This is the banner shown on SSH login: service state, running kernel, subnets and the dashboard address. It also installs the `dashboard-url` command. 2. `Reboot the server now? (y/N)` — **answer `n`**. We reboot later, after configuration (section 9). If you have no way to answer interactively, feed the answers in: ```bash printf 'y\nn\n' | curl -fsSL --http1.1 https://oxidproxy.com/downloads/install.sh | bash ``` **Check:** ```bash oxidproxy -V dpkg -l | grep -c linux-image-.*oxidproxy-p0f ``` The first command must name a version, the second must print `1`. **Compare the version against the one this instruction was written for** — `2.4.x`, see the top of the file. First two digits match — carry on. They differ — consult the documentation before editing `config.yaml`, and tell the person that the instruction is older than the software. --- ## 4. Generating secrets Run these and keep the values — they are needed in section 6. ```bash openssl rand -hex 24 ``` → this is the **master_token** (the API and dashboard token). ```bash openssl rand -base64 18 | tr -d '/+=' ``` → this is the **service account password**. ```bash od -An -N8 -tu8 /dev/urandom | tr -d ' ' ``` → this is the **account_seed**. If `openssl` is missing: `apt-get install -y openssl`. --- ## 5. Editing the configuration The file is `/etc/oxidproxy/config.yaml`. It is already in place — a template with placeholder values. Change **only the fields listed below**; leave the rest alone. ### 5.1. Required replacements | Field | Template value | Set to | |---|---|---| | `license_key` | `"PUT_YOUR_KEY_HERE"` | the customer's key | | `public_ip` | `"YOUR_EXTERNAL_IPV4"` | the external IPv4 | | `master_token` | `"change_me_to_secure_token"` | the generated token | | `max_accounts` | `100000` | the customer's answer, or `10000` | | `account_seed` | `123456789` | the generated number | | `service_account.password` | `"strong_password"` | the generated password | **Not one of the left-hand values may remain in the file.** You verify this in section 8. ### 5.2. IPv6 subnets Replace the whole `ipv6_subnets` block with the customer's subnets: ```yaml ipv6_subnets: - cidr: "2a01:1234::/32" weight: 100 - cidr: "2a02:5678:9abc::/48" weight: 50 ``` **Delete** the `2a01:xxxx::/32` example line from the template — with it in place the server starts, but goes out to nowhere. ### 5.3. DNS If the customer named their own servers, replace the list. If not, keep it as it is but add the hosting provider's DNS first, with a large weight: ```yaml servers: - address: "10.0.0.53:53" # provider DNS from /etc/resolv.conf weight: 1000 - address: "[2606:4700:4700::1111]:53" weight: 58 - address: "1.1.1.1:53" weight: 7 # the servers below are fallbacks, with no weight - address: "[2620:fe::fe]:53" - address: "9.9.9.9:53" ``` The rule: primary servers have a `weight`, fallbacks do not. Fallbacks are used only when the primaries are unreachable. ### 5.4. Branding If the customer wants their own name, add the section (it is commented out in the template): ```yaml brand: name: "Acme" accent: "Proxy" url: "https://acme.example" ``` `name` + `accent` are the two halves of the wordmark; the second is painted in the accent colour. `url` is where the logo links to — `http://` or `https://` only. If there is no name of their own, **do not add the section at all**. The same wordmark reaches the SSH login banner. That is, the customer's own customer, once logged in to the server, sees their brand and not ours. ### 5.5. What to leave alone `listen_ip`, `accounts_file`, `rotation_interval`, `shaping`, `timeouts`, `log`, `blocked_file`, and the `api_port` and `api_port_https` ports — do not touch these unless the person asks specifically. --- ## 6. Routing the subnet — `/etc/rc.local` Without this step the server cannot take addresses from the subnet. The installer has already put a commented-out example line into `/etc/rc.local`, after the `# --- oxidproxy-ipv6-setup ---` marker. Replace it with **one line per customer subnet**: ```sh # --- oxidproxy-ipv6-setup --- ip -6 route add local 2a01:1234::/32 dev lo ip -6 route add local 2a02:5678:9abc::/48 dev lo ``` The lines must come **before** `exit 0`. The file must be executable: ```bash chmod +x /etc/rc.local ``` Apply immediately, without waiting for a reboot — run the same commands by hand: ```bash ip -6 route add local 2a01:1234::/32 dev lo ``` **Check — each subnet separately:** ```bash for net in 2a01:1234::/32 2a02:5678:9abc::/48; do printf '%-28s %s\n' "$net" "$(ip -6 route show table local | grep -c "$net")" done ``` Each subnet must show a number greater than zero. A zero means that subnet will not work, and the server will not say a word about it. --- ## 7. The MITM port (only if the customer asked for it) Uncomment the `mitm_listeners` section in the configuration and set a free port: ```yaml mitm_listeners: - port: 9443 profiles: - "windows-chrome" - "windows-firefox" - "macos-safari" - "android-chrome" - "ios-safari" default: "windows-chrome" ``` Warn the person about two things: - a client of the MITM port must **install our root certificate once**, or the browser will complain. The certificate is served at `http://:8080/ca`; - curl, scripts and anything else without a browser `User-Agent` **will not pass** through the MITM port — they get a page explaining why. If that is needed, add `allow_unknown_ua: true` to the listener. --- ## 8. Checking the configuration before starting ```bash oxidproxy -t /etc/oxidproxy/config.yaml ``` The command must report that the configuration is accepted. If it does not, read what it objects to and fix that; do not start the service with a configuration it rejected. Separately, make sure no placeholder value is left behind: ```bash grep -nE "PUT_YOUR_KEY_HERE|YOUR_EXTERNAL_IPV4|change_me_to_secure_token|strong_password|2a01:xxxx" /etc/oxidproxy/config.yaml ``` The output must be **empty**. If anything turned up, go back to section 5. --- ## 9. Reboot The kernel carrying the OS fingerprint patch only takes effect after a reboot. Warn the person that the connection will drop for a minute. ```bash reboot ``` Once you are back, **check**: ```bash uname -r ``` The answer must contain `oxidproxy-p0f`. If the suffix is absent, OS fingerprint emulation **is not working** — see section 12. --- ## 10. Starting up and confirming everything is alive ```bash systemctl start oxidproxy ``` Wait 15 seconds and read the journal — this step is mandatory, do not skip it: ```bash sleep 15 journalctl -u oxidproxy -n 30 --no-pager ``` **What must be in the journal:** - `License Activated via ...` — the licence was accepted; - `API Listening (HTTPS) on ...`; - `System Ready. Starting parallel listeners...`. **What must not be there:** lines containing `rejected` or `FATAL`. The service can count as "running" without having raised a single port — that is exactly what makes it deceptive. **Check that the ports really are listening:** ```bash ss -tlnp | grep oxidproxy ``` Every port from the configuration must be visible. **Check that traffic really goes out.** Take the first account and go outside through it: ```bash CRED=$(head -1 /var/lib/oxidproxy/accounts_*_1080.txt | sed 's|socks5://||') curl -s --max-time 20 -x "socks5h://$CRED" https://ipv6.icanhazip.com/ ``` The answer must be an address **from the customer's subnet**. If there is no answer — section 12. **Open the ports in the firewall**, if one is enabled. Check: ```bash ufw status 2>/dev/null || iptables -L -n | head -20 ``` You need the proxy ports (1080–1084, or the custom ones), `api_port_https` (8443) and, if MITM is enabled, its port. --- ## 11. What to hand the person at the end ### 11.1. If the person decided to finish the setup themselves The software is installed but **not running**. Hand them: - what is left to fill in `/etc/oxidproxy/config.yaml` — list the specific fields from section 5.1 that you did not fill in; - whether they still need to add the subnets to `/etc/rc.local` (section 6); - the values you generated, if you managed to write them in; - the commands from the list below. Warn them: while placeholder values from the template remain in the configuration, the service will either fail to start or start to no purpose. ### 11.2. If the deployment was carried through **Check the link before you hand it over.** The string must not merely be assembled from parts — it must actually open the dashboard: ```bash curl -sk -o /dev/null -w '%{http_code}\n' \ "https://:8443/dashboard?token=" ``` It must be `200`. If the code is anything else, do not hand the link over — go to section 12. Then give them, in one message: **Dashboard** ``` https://:8443/dashboard?token= ``` The certificate is self-signed — the browser shows a warning once, and it has to be accepted. The token grants full access to the API and the dashboard; keep it like a password. **Useful commands on the server** ```bash # the full dashboard link, if it gets lost dashboard-url # journal: recent events journalctl -u oxidproxy -n 50 --no-pager # journal, live journalctl -u oxidproxy -f # errors from the last day only journalctl -u oxidproxy --since -1d -p err --no-pager # service state systemctl status oxidproxy # restart after editing the configuration oxidproxy -t /etc/oxidproxy/config.yaml && systemctl restart oxidproxy # which ports are listening ss -tlnp | grep oxidproxy # check which address traffic goes out from curl -s -x "socks5h://LOGIN:PASSWORD@:1080" https://ipv6.icanhazip.com/ ``` Mention separately: **an SSH login as root shows a banner** — service state, running kernel, subnets and the dashboard address. It is the quickest way to tell whether the server is alive. Accounts are created and issued **through the dashboard** — there is no need to open the files in `/var/lib/oxidproxy/` by hand. The login and password of the service account for checkers and monitoring: `checker` and the password you generated; it can reach the `ifconfig.me` domain only. --- ## 12. If something did not work | Symptom | Cause and what to do | |---|---| | `rejected (hwid_mismatch)` in the journal | The key is bound to a different machine. Have the customer contact support for a re-binding. Do not fix it yourself. | | `rejected` in the journal for another reason | The key is wrong or expired. Check that it was copied in full, with no spaces. | | `uname -r` without `oxidproxy-p0f` | The wrong kernel booted. Check `ls /boot/vmlinuz-*` and `GRUB_DEFAULT=0` in `/etc/default/grub`, then `update-grub` and reboot. | | The service "runs" but there are no ports | Almost always the licence. Read the journal, section 10. | | A request through the proxy does not go through | Check the route: `ip -6 route show table local`. If the subnet is not there — section 6. Then check that the server has IPv6 connectivity at all: `curl -6 -s https://ipv6.icanhazip.com/`. | | `oxidproxy -t` complains about `accounts_file` | If there will be several files, the name must contain all three variables: `[os]`, `[mode]`, `[port]`. | | The dashboard does not open from outside | Port `8443` is closed by the firewall or by the hosting provider. | ### Subnet assigned but not routed to the server A case of its own. It looks like a fault in the software, and is not. The symptoms line up like this: - the route is in place, `ip -6 route show table local` shows the subnet; - the ports are listening, there are no errors in the journal; - a request through the proxy (section 10) silently fails; - and above all: **the machine has no global IPv6 address of its own** — `ip -6 addr show scope global` is empty, or holds only `fe80::`. That means the provider assigned the subnet "to the segment" rather than routing it to the server: packets reach the neighbouring router, which asks around "whose address is this?" — and nobody answers. This is not fixable through the software's settings. There are two ways out, both outside it. **1. The right one — ask the provider to route the subnet to the server** (routed, not on-link). Many will do it on request, and then nothing extra needs installing. **2. The workaround — run an NDP proxy daemon** so that it answers the neighbour solicitations on our behalf. ```bash apt-get install -y ndppd ``` `/etc/ndppd.conf` — substitute your own interface and your own subnet: ``` route-ttl 30000 proxy eth0 { router no timeout 500 ttl 30000 rule 2a05:fb42:28e::/48 { static } } ``` ```bash systemctl enable --now ndppd ``` Two things that must not be guessed at: - **`eth0` is the name of the external interface**, and it is often something else. Look at `ip -6 route show default` or `ip -o link`. Get it wrong and the daemon starts up and stays silent. - **`router no`, not `yes`.** Our server is not a router; it merely answers on behalf of the subnet's addresses. With `yes` it starts advertising itself as a router on somebody else's network. Give each customer subnet **its own `rule` block**. **Agent: do not install this on your own — ask first.** Show the person the output of `ip -6 addr show scope global` and `ip -6 route show`, name the diagnosis and both options. The second one touches the server's network configuration and may be against the provider's rules, so do it only on explicit consent. --- If nothing helped, collect the output of `journalctl -u oxidproxy -n 100 --no-pager` and `oxidproxy -V` and send it to support: support@oxidproxy.com --- ## What NOT to do - Do not enable `mitm_accept_invalid_certs` — it is a debugging flag, and it turns off verification of the upstream server's authenticity. - Do not change `tcp_congestion`, `tcp_window_size`, `latency_base_ms` or `tos` in the listeners. That is fine-grained tuning of network behaviour; changing it blindly does harm. - Do not add `jitter_ms`. Since 2.2.0 it is not applied at all: the key is still accepted so that older configurations keep working, but the service logs a warning about it at startup. Finding it in an existing config is not an error — leave it alone. - Do not leave the template `master_token` in place. - Do not publish the contents of `config.yaml` — it holds the licence key and the token.