Skip to content

Commit

Permalink
Add support for http proxy (#68)
Browse files Browse the repository at this point in the history
* 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>
  • Loading branch information
3 people committed May 22, 2023
1 parent d9c6eb7 commit 25e6568
Show file tree
Hide file tree
Showing 7 changed files with 256 additions and 4 deletions.
10 changes: 9 additions & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
16 changes: 13 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down
29 changes: 29 additions & 0 deletions config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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,
Expand Down
156 changes: 156 additions & 0 deletions http.go
Original file line number Diff line number Diff line change
@@ -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)
}
}
16 changes: 16 additions & 0 deletions routine.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
8 changes: 8 additions & 0 deletions test_config.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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 changes: 25 additions & 0 deletions util.go
Original file line number Diff line number Diff line change
@@ -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)),
}
}

0 comments on commit 25e6568

Please sign in to comment.