Skip to content

Transport: Add transparent proxy support for OpenBSD (pf divert-to) - #6546

Open
adsdevdoo wants to merge 1 commit into
XTLS:mainfrom
adsdevdoo:openbsd-transparent-proxy
Open

Transport: Add transparent proxy support for OpenBSD (pf divert-to)#6546
adsdevdoo wants to merge 1 commit into
XTLS:mainfrom
adsdevdoo:openbsd-transparent-proxy

Conversation

@adsdevdoo

Copy link
Copy Markdown

Right now Xray can't work as a transparent proxy on OpenBSD. GetOriginalDestination for TCP is a stub that returns an empty destination, and the UDP hub never learns where a packet was originally headed, so a tunnel inbound with followRedirect: true dies with "unable to get destination". The usual workaround, followRedirect: false plus sniffing, only covers HTTP and TLS. SSH and anything else sniffing can't identify ends up going to 127.0.0.1 and silently fails.

This PR fills in the missing pieces. The mechanisms differ from both Linux and FreeBSD, which I suspect is why nobody ported this before:

  • TCP original destination. pf divert-to does not rewrite the packet, so getsockname(2) on the accepted socket still returns the original destination. This is documented in pf.conf(5) and it's how relayd does it, so transport/internet/tcp/sockopt_openbsd.go just reads conn.LocalAddr(). No natlook ioctl needed, unlike FreeBSD.
  • UDP original destination. applyInboundSocketOptions sets IP_RECVDSTADDR and IP_RECVDSTPORT when ReceiveOriginalDestAddress is requested, and the UDP hub parses both control messages. There is no single cmsg carrying address and port together like Linux's IP_RECVORIGDSTADDR; IP_RECVDSTPORT is OpenBSD specific and both messages are required.
  • UDP replies. A reply has to leave with the original destination as its source address, otherwise the client drops it. FakeUDP for OpenBSD binds a socket to that address with SO_BINDANY, and a pf divert-reply rule gets the packet back to the client.

Everything sits behind openbsd build tags with matching exclusions in the *_other.go files, so no other platform is touched. There's a small unit test for the control message parsing.

No config changes are needed, the existing tunnel inbound options simply start working:

{
  "listen": "127.0.0.1",
  "port": 12345,
  "protocol": "tunnel",
  "settings": { "allowedNetwork": "tcp", "followRedirect": true }
},
{
  "listen": "127.0.0.1",
  "port": 12346,
  "protocol": "tunnel",
  "settings": { "allowedNetwork": "udp", "followRedirect": true }
}

with pf rules along these lines:

pass in quick on $lan_if inet proto tcp from $lan_net to ! <bypass> divert-to 127.0.0.1 port 12345
pass in quick on $lan_if inet proto udp from $lan_net to ! <bypass> divert-to 127.0.0.1 port 12346
pass out quick on $lan_if inet proto udp from ! <bypass> to $lan_net divert-reply

Testing. This is running on two OpenBSD routers, 7.7 and 7.8, both amd64, each serving a small LAN. Verified there: plain web traffic, SSH out to external hosts and other protocols sniffing cannot classify, DNS, QUIC/HTTP3, and Telegram and WhatsApp voice calls. go test ./transport/internet/udp passes on OpenBSD, and gofmt and go vet are clean on the touched packages. I have not run it on 7.9 yet. I went through the 7.9 release notes and changelog and found nothing touching divert, SO_BINDANY or the recvdst options, so I expect it to work unchanged, and I'll report back once I've upgraded a box.

Limitations, to be upfront about them:

  • IPv4 only. FakeUDP returns a clear error for IPv6 targets. Doing v6 properly means IPV6_RECVPKTINFO based plumbing, and I didn't want to ship code I can't test.
  • SO_BINDANY needs root, but that's inherent to transparent proxying on OpenBSD, relayd has the same requirement.

One more thing. #5824 moves OpenBSD out of regular releases for lack of manpower, and says the platform can rejoin the standard build process once it gets reasonably active support. I run Xray on OpenBSD daily and I'm willing to be that support: testing on real hardware, keeping the OpenBSD specific code building and working across releases, and picking up OpenBSD issues as they come in. Happy to adjust naming or structure if you'd rather have this organized differently.

@RPRX

RPRX commented Jul 27, 2026

Copy link
Copy Markdown
Member

Rebase

现在 TUN 有 FreeBSD,TPROXY 有 OpenBSD

@adsdevdoo
adsdevdoo force-pushed the openbsd-transparent-proxy branch from ef21c99 to 9458117 Compare July 27, 2026 14:29
@Fangliding

Copy link
Copy Markdown
Member

GetOriginalDestination 是 redirect 模式使用的 从 conn.LocalAddr() 拿地址是典型的 Tproxy 模式的特征 它就在下面 从conn.LocalAddr() 获取地址 你为什么要单独写一个 redirect 模式 用的 GetOriginalDestination 然后包装 conn.LocalAddr() ?

Implement the OpenBSD side of transparent proxying for the tunnel
(dokodemo-door) inbound with followRedirect enabled:

- UDP: enable IP_RECVDSTADDR and IP_RECVDSTPORT on the listening socket
  and read the original destination from the control messages. OpenBSD
  has no single message carrying address and port together, so both are
  required.
- UDP replies: implement FakeUDP with SO_BINDANY so replies leave with
  the original destination as their source address; pf routes them back
  to the client via divert-reply.

TCP needs no code: pf divert-to leaves the packet untouched, so the
local address of the accepted socket already is the original
destination, which is what the existing TProxy branch reads. Such an
inbound is configured with "tproxy": "tproxy" in its sockopt.

IPv4 only for now; IPv6 returns a clear error from FakeUDP.
@adsdevdoo
adsdevdoo force-pushed the openbsd-transparent-proxy branch from 9458117 to 1e87ebc Compare July 28, 2026 06:11
@adsdevdoo

adsdevdoo commented Jul 28, 2026

Copy link
Copy Markdown
Author

@Fangliding
Good catch, you're right. divert-to doesn't rewrite the packet, so the socket's local address is already the original destination, which is exactly what the tproxy branch does. Dropped that file and the build tag change with it, the inbound just needs "tproxy": "tproxy" in sockopt now. Retested on the router afterwards.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants