Add support for http proxy (#68)

* Add support for http proxy

* add test case for http proxy

---------

Co-authored-by: octeep <github@bandersnatch.anonaddy.com>
Co-authored-by: pufferfish <74378430+pufferffish@users.noreply.github.com>
This commit is contained in:
Wayback Archiver 2023-05-22 16:47:33 +00:00 committed by GitHub
parent d9c6eb7143
commit 25e6568f4d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 256 additions and 4 deletions

View file

@ -29,4 +29,12 @@ jobs:
run: ./wireproxy -c test.conf & sleep 1
- name: Test socks5
run: curl --proxy socks5://localhost:64423 http://zx2c4.com/ip | grep -q "demo.wireguard.com"
- name: Test http
run: curl --proxy http://localhost:64424 http://zx2c4.com/ip | grep -q "demo.wireguard.com"
- name: Test http with password
run: curl --proxy http://peter:hunter123@localhost:64424 http://zx2c4.com/ip | grep -q "demo.wireguard.com"
- name: Test http with wrong password
run: |
set +e
curl -s --fail --proxy http://peter:wrongpass@localhost:64425 http://zx2c4.com/ip
if [[ $? == 0 ]]; then exit 1; fi

View file

@ -3,11 +3,11 @@
[![Build status](https://github.com/octeep/wireproxy/actions/workflows/build.yml/badge.svg)](https://github.com/octeep/wireproxy/actions)
[![Documentation](https://img.shields.io/badge/godoc-wireproxy-blue)](https://pkg.go.dev/github.com/octeep/wireproxy)
A wireguard client that exposes itself as a socks5 proxy or tunnels.
A wireguard client that exposes itself as a socks5/http proxy or tunnels.
# What is this
`wireproxy` is a completely userspace application that connects to a wireguard peer,
and exposes a socks5 proxy or tunnels on the machine. This can be useful if you need
and exposes a socks5/http proxy or tunnels on the machine. This can be useful if you need
to connect to certain sites via a wireguard peer, but can't be bothered to setup a new network
interface for whatever reasons.
@ -22,7 +22,7 @@ anything.
# Feature
- TCP static routing for client and server
- SOCKS5 proxy (currently only CONNECT is supported)
- SOCKS5/HTTP proxy (currently only CONNECT is supported)
# TODO
- UDP Support in SOCKS5
@ -100,6 +100,16 @@ BindAddress = 127.0.0.1:25344
#Username = ...
# Avoid using spaces in the password field
#Password = ...
# http creates a http proxy on your LAN, and all traffic would be routed via wireguard.
[http]
BindAddress = 127.0.0.1:25345
# HTTP authentication parameters, specifying username and password enables
# proxy authentication.
#Username = ...
# Avoid using spaces in the password field
#Password = ...
```
Alternatively, if you already have a wireguard config, you can import it in the

View file

@ -45,6 +45,12 @@ type Socks5Config struct {
Password string
}
type HTTPConfig struct {
BindAddress string
Username string
Password string
}
type Configuration struct {
Device *DeviceConfig
Routines []RoutineSpawner
@ -330,6 +336,24 @@ func parseSocks5Config(section *ini.Section) (RoutineSpawner, error) {
return config, nil
}
func parseHTTPConfig(section *ini.Section) (RoutineSpawner, error) {
config := &HTTPConfig{}
bindAddress, err := parseString(section, "BindAddress")
if err != nil {
return nil, err
}
config.BindAddress = bindAddress
username, _ := parseString(section, "Username")
config.Username = username
password, _ := parseString(section, "Password")
config.Password = password
return config, nil
}
// Takes a function that parses an individual section into a config, and apply it on all
// specified sections
func parseRoutinesConfig(routines *[]RoutineSpawner, cfg *ini.File, sectionName string, f func(*ini.Section) (RoutineSpawner, error)) error {
@ -404,6 +428,11 @@ func ParseConfig(path string) (*Configuration, error) {
return nil, err
}
err = parseRoutinesConfig(&routinesSpawners, cfg, "http", parseHTTPConfig)
if err != nil {
return nil, err
}
return &Configuration{
Device: device,
Routines: routinesSpawners,

156
http.go Normal file
View file

@ -0,0 +1,156 @@
package wireproxy
import (
"bufio"
"bytes"
"encoding/base64"
"fmt"
"io"
"log"
"net"
"net/http"
"strings"
)
const proxyAuthHeaderKey = "Proxy-Authorization"
type HTTPServer struct {
config *HTTPConfig
auth CredentialValidator
dial func(network, address string) (net.Conn, error)
authRequired bool
}
func (s *HTTPServer) authenticate(req *http.Request) (int, error) {
if !s.authRequired {
return 0, nil
}
auth := req.Header.Get(proxyAuthHeaderKey)
if auth != "" {
enc := strings.TrimPrefix(auth, "Basic ")
str, err := base64.StdEncoding.DecodeString(enc)
if err != nil {
return http.StatusNotAcceptable, fmt.Errorf("decode username and password failed: %w", err)
}
pairs := bytes.SplitN(str, []byte(":"), 2)
if len(pairs) != 2 {
return http.StatusLengthRequired, fmt.Errorf("username and password format invalid")
}
if s.auth.Valid(string(pairs[0]), string(pairs[1])) {
return 0, nil
}
return http.StatusUnauthorized, fmt.Errorf("username and password not matching")
}
return http.StatusProxyAuthRequired, fmt.Errorf(http.StatusText(http.StatusProxyAuthRequired))
}
func (s *HTTPServer) handleConn(req *http.Request, conn net.Conn) (peer net.Conn, err error) {
addr := req.Host
if !strings.Contains(addr, ":") {
port := "443"
addr = net.JoinHostPort(addr, port)
}
peer, err = s.dial("tcp", addr)
if err != nil {
return peer, fmt.Errorf("tun tcp dial failed: %w", err)
}
_, err = conn.Write([]byte("HTTP/1.1 200 Connection established\r\n\r\n"))
if err != nil {
peer.Close()
peer = nil
}
return
}
func (s *HTTPServer) handle(req *http.Request) (peer net.Conn, err error) {
addr := req.Host
if !strings.Contains(addr, ":") {
port := "80"
addr = net.JoinHostPort(addr, port)
}
peer, err = s.dial("tcp", addr)
if err != nil {
return peer, fmt.Errorf("tun tcp dial failed: %w", err)
}
err = req.Write(peer)
if err != nil {
peer.Close()
peer = nil
return peer, fmt.Errorf("conn write failed: %w", err)
}
return
}
func (s *HTTPServer) serve(conn net.Conn) error {
defer conn.Close()
var rd io.Reader = bufio.NewReader(conn)
req, err := http.ReadRequest(rd.(*bufio.Reader))
if err != nil {
return fmt.Errorf("read request failed: %w", err)
}
code, err := s.authenticate(req)
if err != nil {
_ = responseWith(req, code).Write(conn)
return err
}
var peer net.Conn
switch req.Method {
case http.MethodConnect:
peer, err = s.handleConn(req, conn)
case http.MethodGet:
peer, err = s.handle(req)
default:
_ = responseWith(req, http.StatusMethodNotAllowed).Write(conn)
return fmt.Errorf("unsupported protocol: %s", req.Method)
}
if err != nil {
return fmt.Errorf("dial proxy failed: %w", err)
}
if peer == nil {
return fmt.Errorf("dial proxy failed: peer nil")
}
defer peer.Close()
go func() {
defer peer.Close()
defer conn.Close()
_, _ = io.Copy(conn, peer)
}()
_, err = io.Copy(peer, conn)
return err
}
// ListenAndServe is used to create a listener and serve on it
func (s *HTTPServer) ListenAndServe(network, addr string) error {
server, err := net.Listen("tcp", s.config.BindAddress)
if err != nil {
return fmt.Errorf("listen tcp failed: %w", err)
}
for {
conn, err := server.Accept()
if err != nil {
return fmt.Errorf("accept request failed: %w", err)
}
go func(conn net.Conn) {
err = s.serve(conn)
if err != nil {
log.Println(err)
}
}(conn)
}
}

View file

@ -137,6 +137,22 @@ func (config *Socks5Config) SpawnRoutine(vt *VirtualTun) {
}
}
// SpawnRoutine spawns a http server.
func (config *HTTPConfig) SpawnRoutine(vt *VirtualTun) {
http := &HTTPServer{
config: config,
dial: vt.Tnet.Dial,
auth: CredentialValidator{config.Username, config.Password},
}
if config.Username != "" || config.Password != "" {
http.authRequired = true
}
if err := http.ListenAndServe("tcp", config.BindAddress); err != nil {
log.Fatal(err)
}
}
// Valid checks the authentication data in CredentialValidator and compare them
// to username and password in constant time.
func (c CredentialValidator) Valid(username, password string) bool {

View file

@ -17,4 +17,12 @@ Endpoint = demo.wireguard.com:$server_port
[Socks5]
BindAddress = 127.0.0.1:64423
[http]
BindAddress = 127.0.0.1:64424
[http]
BindAddress = 127.0.0.1:64425
Username = peter
Password = hunter123
EOL

25
util.go Normal file
View file

@ -0,0 +1,25 @@
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)),
}
}