From 25e6568f4d6f1ab9797762391cda6ac6d39da8be Mon Sep 17 00:00:00 2001 From: Wayback Archiver <66856220+waybackarchiver@users.noreply.github.com> Date: Mon, 22 May 2023 16:47:33 +0000 Subject: [PATCH] Add support for http proxy (#68) * Add support for http proxy * add test case for http proxy --------- Co-authored-by: octeep Co-authored-by: pufferfish <74378430+pufferffish@users.noreply.github.com> --- .github/workflows/test.yml | 10 ++- README.md | 16 +++- config.go | 29 +++++++ http.go | 156 +++++++++++++++++++++++++++++++++++++ routine.go | 16 ++++ test_config.sh | 8 ++ util.go | 25 ++++++ 7 files changed, 256 insertions(+), 4 deletions(-) create mode 100644 http.go create mode 100644 util.go diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 6c46d20..0a55f77 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -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 diff --git a/README.md b/README.md index 9a46a13..c99a939 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/config.go b/config.go index a52431c..81f00d6 100644 --- a/config.go +++ b/config.go @@ -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, diff --git a/http.go b/http.go new file mode 100644 index 0000000..fcdab44 --- /dev/null +++ b/http.go @@ -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) + } +} diff --git a/routine.go b/routine.go index c45b301..413b76f 100644 --- a/routine.go +++ b/routine.go @@ -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 { diff --git a/test_config.sh b/test_config.sh index 3f439cd..ae18b12 100755 --- a/test_config.sh +++ b/test_config.sh @@ -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 diff --git a/util.go b/util.go new file mode 100644 index 0000000..89095eb --- /dev/null +++ b/util.go @@ -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)), + } +}