| | package main |
| |
|
| | import ( |
| | "encoding/base64" |
| | "encoding/hex" |
| | "fmt" |
| | "io" |
| | "log" |
| | "net" |
| | "net/http" |
| | "net/netip" |
| | "strings" |
| | "time" |
| |
|
| | "github.com/caarlos0/env" |
| | "golang.zx2c4.com/wireguard/conn" |
| | "golang.zx2c4.com/wireguard/device" |
| | "golang.zx2c4.com/wireguard/tun/netstack" |
| | ) |
| |
|
| | type params struct { |
| | User string `env:"PROXY_USER" envDefault:""` |
| | Password string `env:"PROXY_PASS" envDefault:""` |
| | Port string `env:"PORT" envDefault:"8080"` |
| | |
| | WgPrivateKey string `env:"WIREGUARD_INTERFACE_PRIVATE_KEY"` |
| | WgAddress string `env:"WIREGUARD_INTERFACE_ADDRESS"` |
| | WgPeerPublicKey string `env:"WIREGUARD_PEER_PUBLIC_KEY"` |
| | WgPeerEndpoint string `env:"WIREGUARD_PEER_ENDPOINT"` |
| | WgDNS string `env:"WIREGUARD_INTERFACE_DNS" envDefault:"1.1.1.1"` |
| | } |
| |
|
| | var tnet *netstack.Net |
| |
|
| | func handleTunneling(w http.ResponseWriter, r *http.Request) { |
| | dest := r.URL.Host |
| | if dest == "" { |
| | dest = r.Host |
| | } |
| |
|
| | |
| | hijacker, ok := w.(http.Hijacker) |
| | if !ok { |
| | http.Error(w, "Hijacking not supported", http.StatusInternalServerError) |
| | return |
| | } |
| | client_conn, _, err := hijacker.Hijack() |
| | if err != nil { |
| | |
| | http.Error(w, err.Error(), http.StatusServiceUnavailable) |
| | return |
| | } |
| |
|
| | var dest_conn net.Conn |
| |
|
| | if tnet == nil { |
| | dest_conn, err = net.DialTimeout("tcp", dest, 10*time.Second) |
| | } else { |
| | |
| | dest_conn, err = tnet.Dial("tcp", dest) |
| | } |
| |
|
| | if err != nil { |
| | log.Printf("[ERROR] TUNNEL Dial failed to %s: %v", dest, err) |
| | |
| | |
| | client_conn.Write([]byte("HTTP/1.1 503 Service Unavailable\r\n\r\n")) |
| | client_conn.Close() |
| | return |
| | } |
| |
|
| | |
| | |
| | _, err = client_conn.Write([]byte("HTTP/1.1 200 Connection Established\r\n\r\n")) |
| | if err != nil { |
| | log.Printf("[ERROR] TUNNEL Write 200 failed: %v", err) |
| | dest_conn.Close() |
| | client_conn.Close() |
| | return |
| | } |
| |
|
| | go transfer(dest_conn, client_conn) |
| | go transfer(client_conn, dest_conn) |
| | } |
| |
|
| | func transfer(destination io.WriteCloser, source io.ReadCloser) { |
| | defer destination.Close() |
| | defer source.Close() |
| | io.Copy(destination, source) |
| | } |
| |
|
| | func handleHTTP(w http.ResponseWriter, r *http.Request) { |
| | transport := http.DefaultTransport.(*http.Transport).Clone() |
| | |
| | if tnet != nil { |
| | |
| | transport.DialContext = tnet.DialContext |
| | } |
| |
|
| | resp, err := transport.RoundTrip(r) |
| | if err != nil { |
| | http.Error(w, err.Error(), http.StatusServiceUnavailable) |
| | return |
| | } |
| | defer resp.Body.Close() |
| | copyHeader(w.Header(), resp.Header) |
| | w.WriteHeader(resp.StatusCode) |
| | io.Copy(w, resp.Body) |
| | } |
| |
|
| | func handleDebug(w http.ResponseWriter, r *http.Request) { |
| | if tnet == nil { |
| | w.Header().Set("Content-Type", "text/plain") |
| | w.WriteHeader(http.StatusOK) |
| | w.Write([]byte("Error: WireGuard not initialized (Direct Mode)")) |
| | return |
| | } |
| |
|
| | client := &http.Client{ |
| | Transport: &http.Transport{ |
| | DialContext: tnet.DialContext, |
| | }, |
| | Timeout: 10 * time.Second, |
| | } |
| |
|
| | resp, err := client.Get("http://ifconfig.me") |
| | if err != nil { |
| | log.Printf("[DEBUG] VPN Test Failed: %v", err) |
| | http.Error(w, fmt.Sprintf("VPN Connection Failed: %v", err), http.StatusServiceUnavailable) |
| | return |
| | } |
| | defer resp.Body.Close() |
| | |
| | body, err := io.ReadAll(resp.Body) |
| | if err != nil { |
| | http.Error(w, fmt.Sprintf("Failed to read response: %v", err), http.StatusInternalServerError) |
| | return |
| | } |
| | |
| | log.Printf("[DEBUG] VPN Test Success. IP: %s", string(body)) |
| | w.Header().Set("Content-Type", "text/plain") |
| | w.WriteHeader(http.StatusOK) |
| | w.Write([]byte(fmt.Sprintf("VPN Connected! Public IP: %s", string(body)))) |
| | } |
| |
|
| | func copyHeader(dst, src http.Header) { |
| | for k, vv := range src { |
| | for _, v := range vv { |
| | dst.Add(k, v) |
| | } |
| | } |
| | } |
| |
|
| | func startWireGuard(cfg params) error { |
| | if cfg.WgPrivateKey == "" || cfg.WgPeerEndpoint == "" { |
| | log.Println("[INFO] WireGuard config missing, running in DIRECT mode (no VPN)") |
| | return nil |
| | } |
| |
|
| | log.Println("[INFO] Initializing Userspace WireGuard...") |
| |
|
| | localIPs := []netip.Addr{} |
| | if cfg.WgAddress != "" { |
| | |
| | addrStr := strings.Split(cfg.WgAddress, "/")[0] |
| | addr, err := netip.ParseAddr(addrStr) |
| | if err == nil { |
| | localIPs = append(localIPs, addr) |
| | log.Printf("[INFO] Local VPN IP: %s", addr) |
| | } else { |
| | log.Printf("[WARN] Failed to parse local IP: %v", err) |
| | } |
| | } |
| | |
| | dnsIP, err := netip.ParseAddr(cfg.WgDNS) |
| | if err != nil { |
| | log.Printf("[WARN] Failed to parse DNS IP, using default: %v", err) |
| | dnsIP, _ = netip.ParseAddr("1.1.1.1") |
| | } |
| | log.Printf("[INFO] DNS Server: %s", dnsIP) |
| |
|
| | log.Println("[INFO] Creating virtual network interface...") |
| | tunDev, tnetInstance, err := netstack.CreateNetTUN( |
| | localIPs, |
| | []netip.Addr{dnsIP}, |
| | 1420, |
| | ) |
| | if err != nil { |
| | return fmt.Errorf("failed to create TUN: %w", err) |
| | } |
| | tnet = tnetInstance |
| | log.Println("[INFO] Virtual TUN device created successfully") |
| |
|
| | log.Println("[INFO] Initializing WireGuard device...") |
| | dev := device.NewDevice(tunDev, conn.NewDefaultBind(), device.NewLogger(device.LogLevelSilent, "")) |
| | |
| | log.Printf("[INFO] Configuring peer endpoint: %s", cfg.WgPeerEndpoint) |
| |
|
| | |
| | |
| | privateKeyHex, err := base64ToHex(cfg.WgPrivateKey) |
| | if err != nil { |
| | return fmt.Errorf("invalid private key (base64 decode failed): %w", err) |
| | } |
| |
|
| | publicKeyHex, err := base64ToHex(cfg.WgPeerPublicKey) |
| | if err != nil { |
| | return fmt.Errorf("invalid peer public key (base64 decode failed): %w", err) |
| | } |
| |
|
| | uapi := fmt.Sprintf(`private_key=%s |
| | public_key=%s |
| | endpoint=%s |
| | allowed_ip=0.0.0.0/0 |
| | `, privateKeyHex, publicKeyHex, cfg.WgPeerEndpoint) |
| |
|
| | if err := dev.IpcSet(uapi); err != nil { |
| | return fmt.Errorf("failed to configure device: %w", err) |
| | } |
| | log.Println("[INFO] WireGuard peer configured") |
| | |
| | if err := dev.Up(); err != nil { |
| | return fmt.Errorf("failed to bring up device: %w", err) |
| | } |
| |
|
| | log.Println("[SUCCESS] WireGuard interface is UP - All traffic will route through VPN") |
| | return nil |
| | } |
| |
|
| | func main() { |
| | log.SetFlags(log.LstdFlags | log.Lmsgprefix) |
| | log.Println("[STARTUP] Initializing HTTP Proxy with Userspace WireGuard") |
| | |
| | cfg := params{} |
| | if err := env.Parse(&cfg); err != nil { |
| | log.Printf("[WARN] Config parse warning: %+v\n", err) |
| | } |
| |
|
| | log.Printf("[CONFIG] Proxy Port: %s", cfg.Port) |
| | if cfg.User != "" { |
| | log.Printf("[CONFIG] Authentication: Enabled (user: %s)", cfg.User) |
| | } else { |
| | log.Println("[CONFIG] Authentication: Disabled") |
| | } |
| |
|
| | if err := startWireGuard(cfg); err != nil { |
| | log.Fatalf("[FATAL] Failed to start WireGuard: %v", err) |
| | } |
| |
|
| | log.Printf("[STARTUP] Starting HTTP proxy server on port %s\n", cfg.Port) |
| |
|
| | server := &http.Server{ |
| | Addr: ":" + cfg.Port, |
| | Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { |
| | if cfg.User != "" && cfg.Password != "" { |
| | user, pass, ok := r.BasicAuth() |
| | if !ok || user != cfg.User || pass != cfg.Password { |
| | log.Printf("[AUTH] Unauthorized access attempt from %s", r.RemoteAddr) |
| | w.Header().Set("Proxy-Authenticate", `Basic realm="Proxy"`) |
| | http.Error(w, "Unauthorized", http.StatusProxyAuthRequired) |
| | return |
| | } |
| | } |
| | |
| | |
| | if r.Method == http.MethodConnect { |
| | log.Printf("[CONNECT] %s -> %s", r.RemoteAddr, r.Host) |
| | handleTunneling(w, r) |
| | return |
| | } |
| | |
| | |
| | |
| | if r.URL.Host == "" { |
| | if r.URL.Path == "/" { |
| | log.Printf("[HEALTH] Health check from %s", r.RemoteAddr) |
| | w.WriteHeader(http.StatusOK) |
| | if tnet != nil { |
| | w.Write([]byte("Proxy Running via Userspace WireGuard")) |
| | } else { |
| | w.Write([]byte("Proxy Running in Direct Mode (No VPN)")) |
| | } |
| | return |
| | } |
| | |
| | if r.URL.Path == "/debug" { |
| | log.Printf("[DEBUG] Debug check from %s", r.RemoteAddr) |
| | handleDebug(w, r) |
| | return |
| | } |
| | } |
| |
|
| | |
| | log.Printf("[HTTP] %s %s -> %s", r.Method, r.RemoteAddr, r.URL.String()) |
| | handleHTTP(w, r) |
| | }), |
| | } |
| | |
| | log.Println("[READY] Proxy server is ready to accept connections") |
| | if err := server.ListenAndServe(); err != nil { |
| | log.Fatalf("[FATAL] Server error: %v", err) |
| | } |
| | } |
| |
|
| | func base64ToHex(b64 string) (string, error) { |
| | decoded, err := base64.StdEncoding.DecodeString(b64) |
| | if err != nil { |
| | return "", err |
| | } |
| | return hex.EncodeToString(decoded), nil |
| | } |
| |
|