Files
GHOSTWIRE/visitor.go
T
Daniel Redetzke d9e87a3dfe Dashboard: shows whether this browser is behind the VPN
The card puts the address websites see next to the server's address.
Through a full tunnel both are the server's, and the card says
"You are protected"; directly or through a split tunnel it says
"Not protected", with the visitor's address and its location.
2026-10-06 13:43:56 +03:00

146 lines
3.7 KiB
Go

package main
import (
"context"
"net"
"net/http"
"net/netip"
"sync"
"time"
)
// visitorView tells the dashboard whether the browser asking is behind the
// VPN. Behind it, websites see the server's address, so the card shows the
// visitor's address next to the server's: the same means protected.
type visitorView struct {
Protected bool `json:"protected"`
IP string `json:"ip"` // what websites see; the server's address when protected
ServerIP string `json:"serverIP"` // empty when the endpoint does not resolve
Peer *peerRef `json:"peer"` // the peer whose tunnel the request came through
Location *GeoInfo `json:"location"` // of IP, when not protected
}
type peerRef struct {
ID string `json:"id"`
Name string `json:"name"`
}
// classifyVisitor decides on the address the server sees (remoteIP).
// Through the tunnel that is the peer's tunnel address; the peer is
// protected when its AllowedIPs send all traffic of that family through
// the server. A server behind NAT may instead see its own or its public
// address, when the request loops back through the router.
func classifyVisitor(c *Config, ip, server netip.Addr, own func(netip.Addr) bool) visitorView {
v := visitorView{IP: ip.String()}
if server.IsValid() {
v.ServerIP = server.String()
}
if p := peerByTunnelAddr(c, ip); p != nil {
v.Peer = &peerRef{ID: p.ID, Name: p.Name}
v.Protected = fullTunnel(peerAllowedIPs(c, p), ip.Is6())
} else if !ip.IsLoopback() && !ip.IsLinkLocalUnicast() && (ip == server || own(ip)) {
v.Protected = true
}
if v.Protected && server.IsValid() {
v.IP = v.ServerIP
}
return v
}
func peerByTunnelAddr(c *Config, ip netip.Addr) *Peer {
for i := range c.Peers {
p := &c.Peers[i]
if !p.Enabled {
continue
}
for _, a := range peerAddresses(c, p) {
if a.Addr() == ip {
return p
}
}
}
return nil
}
// fullTunnel reports whether AllowedIPs route all IPv4 (or IPv6) traffic
// into the tunnel: 0.0.0.0/0, ::/0, or the two halves some clients use.
func fullTunnel(allowed []string, v6 bool) bool {
halves := 0
for _, s := range allowed {
p, err := netip.ParsePrefix(s)
if err != nil || p.Addr().Is6() != v6 {
continue
}
switch p.Bits() {
case 0:
return true
case 1:
halves++
}
}
return halves >= 2
}
// ownAddr reports whether ip is an address of this machine.
func ownAddr(ip netip.Addr) bool {
addrs, err := net.InterfaceAddrs()
if err != nil {
return false
}
for _, a := range addrs {
if n, ok := a.(*net.IPNet); ok {
if x, ok := netip.AddrFromSlice(n.IP); ok && x.Unmap() == ip {
return true
}
}
}
return false
}
// endpointIPs resolves the WireGuard endpoint, the address websites see
// behind the VPN, and keeps the answer for a few minutes.
type endpointIPs struct {
mu sync.Mutex
host string
addrs []netip.Addr
at time.Time
}
func (e *endpointIPs) lookup(host string, v6 bool) netip.Addr {
if ip, err := netip.ParseAddr(host); err == nil {
return ip.Unmap()
}
e.mu.Lock()
defer e.mu.Unlock()
if host != e.host || time.Since(e.at) > 10*time.Minute {
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
addrs, _ := net.DefaultResolver.LookupNetIP(ctx, "ip", host)
cancel()
e.host, e.addrs, e.at = host, addrs, time.Now()
}
var other netip.Addr
for _, a := range e.addrs {
a = a.Unmap()
if a.Is6() == v6 {
return a
}
if !other.IsValid() {
other = a
}
}
return other
}
func (a *App) visitor(r *http.Request, c *Config) visitorView {
ip, err := netip.ParseAddr(remoteIP(r))
if err != nil {
return visitorView{IP: remoteIP(r)}
}
ip = ip.Unmap()
v := classifyVisitor(c, ip, a.endpoint.lookup(c.Server.Endpoint, ip.Is6()), ownAddr)
if !v.Protected && v.Peer == nil {
v.Location = a.geo.Lookup(v.IP)
}
return v
}