Enhance VPN health checks and connectivity validation

- **README.md**: Added a 'Fork Features' section to highlight new enhancements like Google health checks, IP validation, and program termination for VPN health checks.
- **main.go**: Integrated new health check logic.
- **http.go**: Added 'responseWith' function and imported 'strconv' for better HTTP response handling.
- **wireguard.go**:
  - Added imports for 'log', 'os', 'strings', and 'time'.
  - Enhanced 'StartWireguard' with handshake verification and Google connectivity checks.
  - Implemented program termination on critical check failures.
- **utils.go**:
  - Renamed from 'util.go'.
  - Added 'CheckGoogleConnectivity' and 'checkIP' functions for external connectivity validation.
- **config.go**: No changes.
- **net.go**: No changes.
- **routine.go**: No changes.

These updates improve the reliability, security, and maintainability of the WireGuard VPN management system by ensuring proper connectivity and health checks.
This commit is contained in:
Yigit Konur 2024-07-05 12:32:12 +01:00
parent e749217090
commit 1805406a04
9 changed files with 154 additions and 31 deletions

BIN
.DS_Store vendored Normal file

Binary file not shown.

2
.gitignore vendored
View file

@ -4,3 +4,5 @@
/.idea
.goreleaser.yml
*.conf
.DS_Store
*/.DS_Store

View file

@ -5,6 +5,12 @@
A wireguard client that exposes itself as a socks5/http proxy or tunnels.
## Fork Features (Added by Wope Team)
This fork introduces several new features to enhance VPN health checks and connectivity validation:
- **Google Connectivity Check**: Ensures the VPN connection is capable of accessing external resources by performing a connectivity check to Google.
- **IP Check Logic**: Verifies the public IP address using an external service to ensure the VPN is correctly routing traffic.
- **Program Termination on Failure**: If critical checks (like handshake and Google connectivity) fail, the program logs detailed error messages and exits, preventing the system from running in an unreliable state.
# What is this
`wireproxy` is a completely userspace application that connects to a wireguard peer,
and exposes a socks5/http proxy or tunnels on the machine. This can be useful if you need
@ -251,4 +257,4 @@ If nothing is set for `CheckAlive`, an empty JSON object with 200 will be the re
The peer which the ICMP ping packet is routed to depends on the `AllowedIPs` set for each peers.
# Stargazers over time
[![Stargazers over time](https://starchart.cc/octeep/wireproxy.svg)](https://starchart.cc/octeep/wireproxy)
[![Stargazers over time](https://starchart.cc/octeep/wireproxy.svg)](https://starchart.cc/octeep/wireproxy)

BIN
cmd/.DS_Store vendored Normal file

Binary file not shown.

View file

@ -10,6 +10,7 @@ import (
"os"
"os/exec"
"os/signal"
"path/filepath"
"strconv"
"syscall"
@ -213,6 +214,9 @@ func main() {
return
}
// Extract the configuration name from the file path
configName := filepath.Base(*config)
// Wireguard doesn't allow configuring which FD to use for logging
// https://github.com/WireGuard/wireguard-go/blob/master/device/logger.go#L39
// so redirect STDOUT to STDERR, we don't want to print anything to STDOUT anyways
@ -224,7 +228,7 @@ func main() {
lock("ready")
tun, err := wireproxy.StartWireguard(conf.Device, logLevel)
tun, err := wireproxy.StartWireguard(conf.Device, logLevel, configName)
if err != nil {
log.Fatal(err)
}
@ -245,4 +249,4 @@ func main() {
}
<-ctx.Done()
}
}

17
http.go
View file

@ -9,6 +9,7 @@ import (
"log"
"net"
"net/http"
"strconv" // Add the missing import
"strings"
"github.com/sourcegraph/conc"
@ -160,3 +161,19 @@ func (s *HTTPServer) ListenAndServe(network, addr string) error {
}(conn)
}
}
// responseWith constructs an HTTP response with the given status code
func responseWith(req *http.Request, statusCode int) *http.Response {
statusText := http.StatusText(statusCode)
body := "wireproxy:" + " " + req.Proto + " " + strconv.Itoa(statusCode) + " " + statusText + "\r\n"
return &http.Response{
StatusCode: statusCode,
Status: statusText,
Proto: req.Proto,
ProtoMajor: req.ProtoMajor,
ProtoMinor: req.ProtoMinor,
Header: http.Header{},
Body: io.NopCloser(bytes.NewBufferString(body)),
}
}

25
util.go
View file

@ -1,25 +0,0 @@
package wireproxy
import (
"bytes"
"io"
"net/http"
"strconv"
)
const space = " "
func responseWith(req *http.Request, statusCode int) *http.Response {
statusText := http.StatusText(statusCode)
body := "wireproxy:" + space + req.Proto + space + strconv.Itoa(statusCode) + space + statusText + "\r\n"
return &http.Response{
StatusCode: statusCode,
Status: statusText,
Proto: req.Proto,
ProtoMajor: req.ProtoMajor,
ProtoMinor: req.ProtoMinor,
Header: http.Header{},
Body: io.NopCloser(bytes.NewBufferString(body)),
}
}

87
utils.go Normal file
View file

@ -0,0 +1,87 @@
package wireproxy
import (
"fmt"
"io/ioutil"
"log"
"net/http"
"os"
"strings"
"golang.zx2c4.com/wireguard/tun/netstack"
)
// CheckGoogleConnectivity checks Google connectivity using the WireGuard tunnel's network stack
func CheckGoogleConnectivity(tun *netstack.Net, configName string) error {
// Step 1: Check IP using icanhazip.com
ip, err := checkIP(tun, configName)
if err != nil {
log.Printf("All retries are completed, VPN is not working. Config name: %s", configName)
os.Exit(1)
}
log.Printf("IP connectivity check successful: %s", ip)
// Step 2: Check Google connectivity with redirect handling
client := &http.Client{
Transport: &http.Transport{
DialContext: tun.DialContext,
},
CheckRedirect: func(req *http.Request, via []*http.Request) error {
// Prevent automatic redirect following
return http.ErrUseLastResponse
},
}
req, err := http.NewRequest("GET", "https://www.google.com/search?q=what+is+my+ip&num=100", nil)
if err != nil {
log.Printf("All retries are completed, VPN is not working. Config name: %s", configName)
os.Exit(1)
}
req.Header.Set("User-Agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36")
resp, err := client.Do(req)
if err != nil {
log.Printf("All retries are completed, VPN is not working. Config name: %s", configName)
os.Exit(1)
}
defer resp.Body.Close()
// Log the status code directly from the first response
log.Printf("Google connectivity check returned status %d", resp.StatusCode)
if resp.StatusCode == 200 {
log.Println("Google connectivity check successful: Status 200")
os.Exit(0)
} else if resp.StatusCode == 302 {
log.Printf("Google connectivity check returned status %d (redirect), terminating process. Config name: %s\n", resp.StatusCode, configName)
os.Exit(1)
} else if resp.StatusCode >= 100 && resp.StatusCode <= 599 {
log.Printf("Google connectivity check returned status %d, terminating process. Config name: %s\n", resp.StatusCode, configName)
os.Exit(1)
} else {
log.Printf("Unexpected status code: %d, terminating process. Config name: %s\n", resp.StatusCode, configName)
os.Exit(1)
}
return nil
}
// checkIP checks the public IP using the WireGuard tunnel's network stack
func checkIP(tun *netstack.Net, configName string) (string, error) {
client := &http.Client{
Transport: &http.Transport{
DialContext: tun.DialContext,
},
}
resp, err := client.Get("https://icanhazip.com")
if err != nil {
return "", fmt.Errorf("Get \"https://icanhazip.com\": %v", err)
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return "", fmt.Errorf("ReadAll: %v", err)
}
return strings.TrimSpace(string(body)), nil // Trim any extra whitespace
}

View file

@ -3,8 +3,11 @@ package wireproxy
import (
"bytes"
"fmt"
"log"
"net/netip"
"os"
"strings"
"time"
"github.com/MakeNowJust/heredoc/v2"
"golang.zx2c4.com/wireguard/conn"
@ -59,7 +62,7 @@ func createIPCRequest(conf *DeviceConfig) (*DeviceSetting, error) {
}
// StartWireguard creates a tun interface on netstack given a configuration
func StartWireguard(conf *DeviceConfig, logLevel int) (*VirtualTun, error) {
func StartWireguard(conf *DeviceConfig, logLevel int, configName string) (*VirtualTun, error) {
setting, err := createIPCRequest(conf)
if err != nil {
return nil, err
@ -80,6 +83,35 @@ func StartWireguard(conf *DeviceConfig, logLevel int) (*VirtualTun, error) {
return nil, err
}
// Ensure handshake is established
for _, peer := range conf.Peers {
if peer.Endpoint != nil {
// Check handshake status
handshakeEstablished := false
for i := 0; i < 3; i++ { // Retry for a few seconds
peerStatus, err := dev.IpcGet()
if err != nil {
return nil, fmt.Errorf("failed to get device status: %w", err)
}
if strings.Contains(peerStatus, *peer.Endpoint) {
handshakeEstablished = true
break
}
time.Sleep(1 * time.Second)
}
if !handshakeEstablished {
log.Printf("All retries are completed, VPN is not working. Config name: %s", configName)
os.Exit(1)
}
}
}
// Perform Google connectivity check only if the handshake is successful
err = CheckGoogleConnectivity(tnet, configName)
if err != nil {
return nil, err
}
return &VirtualTun{
Tnet: tnet,
Dev: dev,
@ -87,4 +119,4 @@ func StartWireguard(conf *DeviceConfig, logLevel int) (*VirtualTun, error) {
SystemDNS: len(setting.dns) == 0,
PingRecord: make(map[string]uint64),
}, nil
}
}