How frp Virtual Network (VirtualNet) Enables Full Layer-3 Connectivity
frp's Virtual Network creates a virtual Layer-3 network by combining a TUN interface on the server with bidirectional IP routing tables, allowing any IP traffic (TCP, UDP, ICMP) to traverse the frp tunnel as if both machines shared the same LAN.
The frp (Fast Reverse Proxy) project by fatedier/frp traditionally provided port-forwarding capabilities, but the Virtual Network feature fundamentally extends this architecture. By implementing a virtual TUN device and intelligent routing logic in the source code, frp enables full IP-level connectivity between client and server machines without requiring modifications to application traffic.
Core Architecture Components
The VirtualNet implementation relies on three tightly-coupled components working across the frp client (frpc) and server (frps):
TUN Interface and VNet Controller
Located in pkg/vnet/controller.go, the VNet Controller manages a virtual TUN network interface on the frps side. This controller reads raw IP packets from the TUN device, parses IPv4/IPv6 headers, and dispatches them to the appropriate tunnel connection. The controller maintains two distinct routing tables to handle traffic flow in both directions.
Visitor Plugin (Client-Side Route Registration)
The visitor plugin in pkg/plugin/visitor/virtual_net.go operates on the frpc side. When enabled, it registers client routes (destination IPs or CIDR blocks) with the VNet Controller using RegisterClientRoute. The plugin receives a pipe-connected net.Conn and implements exponential back-off reconnection logic when the tunnel disconnects. This establishes the outbound path from server to client.
Client Plugin (Server-Side Connection Handling)
On the frps side, pkg/plugin/client/virtual_net.go creates server routes for each incoming virtual_net proxy connection. Through StartServerConnReadLoop, the plugin registers the connection with the controller and continuously reads traffic from the tunnel, feeding it back into the TUN device for delivery to the target virtual IP.
Bidirectional Packet Routing Mechanism
The controller implements a two-router system to achieve full-mesh connectivity:
clientRouter: Routes outbound packets from the virtual network to client connections based on the packet's destination IP usingfindConn(dst).serverRouter: Routes inbound packets to server connections based on the packet's source IP usingfindConnBySrc(src).
When handlePacket processes a packet from the TUN interface in pkg/vnet/controller.go, it first attempts to match the destination IP against the client router:
if targetConn, err := c.clientRouter.findConn(dst); err == nil {
WriteMessage(targetConn, buf)
return
}
If no client route exists, it falls back to the server router to find a connection by source IP:
if targetConn, err := c.serverRouter.findConnBySrc(src); err == nil {
WriteMessage(targetConn, buf)
return
}
log.Tracef("no route found for packet from %s to %s", src, dst)
This dual-router design ensures that traffic originating from either side of the tunnel can reach its destination, enabling protocols like ICMP (ping), UDP, and arbitrary TCP connections that traditional port-forwarding cannot support.
Configuring frp Virtual Network
Server Configuration (frps)
Enable the VirtualNet feature gate and configure a virtual_net plugin proxy:
# frps.toml
featureGates = { VirtualNet = true }
[[proxies]]
name = "vnet-server"
type = "stcp"
secretKey = "your-secret-key"
[proxies.plugin]
type = "virtual_net"
Client Configuration (frpc)
Assign a virtual IP address and configure the visitor plugin:
# frpc.toml
featureGates = { VirtualNet = true }
serverAddr = "x.x.x.x"
serverPort = 7000
virtualNet.address = "100.86.0.2/24"
[[visitors]]
name = "vnet-visitor"
type = "stcp"
serverName = "vnet-server"
secretKey = "your-secret-key"
bindPort = -1
[visitors.plugin]
type = "virtual_net"
destinationIP = "100.86.0.1"
The virtualNet.address assigns an IP to the local TUN interface, while destinationIP registers the route to the remote peer. When the visitor connection closes, the plugin automatically uses exponential back-off to re-establish the route registration.
Summary
- frp Virtual Network operates at Layer-3 (IP) using a TUN interface in
pkg/vnet/controller.go - Bidirectional routing is handled by separate
clientRouter(destination-based) andserverRouter(source-based) tables - The visitor plugin (
pkg/plugin/visitor/virtual_net.go) registers client-side routes withRegisterClientRouteand automatic reconnection logic - The client plugin (
pkg/plugin/client/virtual_net.go) manages server-side connections throughStartServerConnReadLoop - Configuration requires enabling the
VirtualNetfeature gate and assigning virtual IPs in the same CIDR range - Supports any IP protocol including ICMP, UDP, and TCP without application modification
Frequently Asked Questions
What is the difference between frp Virtual Network and standard TCP/UDP proxies?
Standard frp proxies forward specific ports at Layer-4, requiring each service to be mapped individually. Virtual Network operates at Layer-3, creating a virtual LAN where entire IP subnets are reachable. This allows ping (ICMP), dynamic port allocation, and protocols that don't work through traditional NAT traversal according to the fatedier/frp source code.
Does Virtual Network require root or administrative privileges?
Yes. Creating and managing TUN interfaces requires elevated privileges on both the frps server and frpc client machines. The controller in pkg/vnet/controller.go must open the TUN device, which is a privileged operation on Linux, macOS, and Windows systems.
Can multiple clients join the same virtual network?
Yes. Each client registers its unique virtual IP (e.g., 100.86.0.2/24, 100.86.0.3/24) with the server-side router. The clientRouter maintains separate entries for each destination IP, allowing full mesh connectivity between all joined clients and the server within the configured subnet.
How does frp handle packet routing conflicts or missing routes?
When handlePacket cannot find a matching route in either the clientRouter (by destination) or serverRouter (by source), the packet is dropped with a trace log: "no route found for packet from %s to %s". This prevents IP leakage while the exponential back-off mechanism in the visitor plugin ensures routes are re-established automatically after network disruptions.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →