diff --git a/.github/workflows/container.yml b/.github/workflows/container.yml index b965173..18f582b 100644 --- a/.github/workflows/container.yml +++ b/.github/workflows/container.yml @@ -59,6 +59,7 @@ jobs: docker buildx build \ --platform "$BUILD_PLATFORMS" \ --tag "$CONTAINER_NAME:$CONTAINER_TAG" \ + --tag "$CONTAINER_NAME:$GITHUB_SHA" \ --label "org.opencontainers.image.source=${{ github.server_url }}/${{ github.repository }}" \ --label "org.opencontainers.image.documentation=${{ github.server_url }}/${{ github.repository }}" \ --label "org.opencontainers.image.url=${{ github.server_url }}/${{ github.repository }}/packages" \ diff --git a/README.md b/README.md index fd44869..5c774e0 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,5 @@ # wireproxy + [![ISC licensed](https://img.shields.io/badge/license-ISC-blue)](./LICENSE) [![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) @@ -6,12 +7,14 @@ 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/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. # Why you might want this + - You simply want to use wireguard as a way to proxy some traffic. - You don't want root permission just to change wireguard settings. @@ -20,23 +23,33 @@ and configured my browser to use wireproxy for certain sites. It's pretty useful wireproxy is completely isolated from my network interfaces, and I don't need root to configure anything. -Users who want something similar but for Amnezia VPN can use [this fork](https://github.com/juev/wireproxy/tree/feature/amnezia-go) -of wireproxy by [@juev](https://github.com/juev). +Users who want something similar but for Amnezia VPN can use [this fork](https://github.com/artem-russkikh/wireproxy-awg) +of wireproxy by [@artem-russkikh](https://github.com/artem-russkikh). + +# Sponsor + +This project is supported by [IPRoyal](https://iproyal.com/?r=795836). You can get premium quality proxies at unbeatable prices +with a discount using [this referral link](https://iproyal.com/?r=795836)! 🚀 + +![IPRoyal](/assets/iproyal.png) # Feature + - TCP static routing for client and server - SOCKS5/HTTP proxy (currently only CONNECT is supported) # TODO + - UDP Support in SOCKS5 - UDP static routing # Usage -``` -./wireproxy -c [path to config] + +```bash +./wireproxy [-c path to config] ``` -``` +```bash usage: wireproxy [-h|--help] [-c|--config ""] [-s|--silent] [-d|--daemon] [-i|--info ""] [-v|--version] [-n|--configtest] @@ -47,27 +60,36 @@ Arguments: -h --help Print help information -c --config Path of configuration file + Default paths: /etc/wireproxy/wireproxy.conf, $HOME/.config/wireproxy.conf -s --silent Silent mode -d --daemon Make wireproxy run in background -i --info Specify the address and port for exposing health status -v --version Print version -n --configtest Configtest mode. Only check the configuration file for validity. - ``` # Build instruction -``` + +```bash git clone https://github.com/octeep/wireproxy cd wireproxy make ``` +# Install + +```bash +go install github.com/pufferffish/wireproxy/cmd/wireproxy@v1.0.9 # or @latest +``` + # Use with VPN + Instructions for using wireproxy with Firefox container tabs and auto-start on MacOS can be found [here](/UseWithVPN.md). # Sample config file -``` + +```ini # The [Interface] and [Peer] configurations follow the same semantics and meaning # of a wg-quick configuration. To understand what these fields mean, please refer to: # https://wiki.archlinux.org/title/WireGuard#Persistent_configuration @@ -76,6 +98,7 @@ Instructions for using wireproxy with Firefox container tabs and auto-start on M Address = 10.200.200.2/32 # The subnet should be /32 and /128 for IPv4 and v6 respectively # MTU = 1420 (optional) PrivateKey = uCTIK+56CPyCvwJxmU5dBfuyJvPuSXAq1FzHdnIxe1Q= +# PrivateKey = $MY_WIREGUARD_PRIVATE_KEY # Alternatively, reference environment variables DNS = 10.200.200.1 [Peer] @@ -133,7 +156,8 @@ BindAddress = 127.0.0.1:25345 Alternatively, if you already have a wireguard config, you can import it in the wireproxy config file like this: -``` + +```ini WGConfig = # Same semantics as above @@ -149,7 +173,8 @@ WGConfig = Having multiple peers is also supported. `AllowedIPs` would need to be specified such that wireproxy would know which peer to forward to. -``` + +```ini [Interface] Address = 10.254.254.40/32 PrivateKey = XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX= @@ -181,7 +206,8 @@ Target = service-three.servicenet:80 ``` Wireproxy can also allow peers to connect to it: -``` + +```ini [Interface] ListenPort = 5400 ... @@ -191,7 +217,9 @@ PublicKey = YYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYY= AllowedIPs = 10.254.254.100/32 # Note there is no Endpoint defined here. ``` + # Health endpoint + Wireproxy supports exposing a health endpoint for monitoring purposes. The argument `--info/-i` specifies an address and port (e.g. `localhost:9080`), which exposes a HTTP server that provides health status metric of the server. @@ -202,7 +230,8 @@ Currently two endpoints are implemented: `/readyz`: This responds with a json which shows the last time a pong is received from an IP specified with `CheckAlive`. When `CheckAlive` is set, a ping is sent out to addresses in `CheckAlive` per `CheckAliveInterval` seconds (defaults to 5) via wireguard. If a pong has not been received from one of the addresses within the last `CheckAliveInterval` seconds (+2 seconds for some leeway to account for latency), then it would respond with a 503, otherwise a 200. For example: -``` + +```ini [Interface] PrivateKey = censored Address = 10.2.0.2/32 @@ -218,8 +247,10 @@ Endpoint = 149.34.244.174:51820 [Socks5] BindAddress = 127.0.0.1:25344 ``` + `/readyz` would respond with -``` + +```text < HTTP/1.1 503 Service Unavailable < Date: Thu, 11 Apr 2024 00:54:59 GMT < Content-Length: 35 @@ -229,15 +260,18 @@ BindAddress = 127.0.0.1:25344 ``` And for: -``` + +```ini [Interface] PrivateKey = censored Address = 10.2.0.2/32 DNS = 10.2.0.1 CheckAlive = 1.1.1.1 ``` + `/readyz` would respond with -``` + +```text < HTTP/1.1 200 OK < Date: Thu, 11 Apr 2024 00:56:21 GMT < Content-Length: 23 @@ -251,4 +285,5 @@ 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) diff --git a/UseWithVPN.md b/UseWithVPN.md index cb50538..6257258 100644 --- a/UseWithVPN.md +++ b/UseWithVPN.md @@ -1,11 +1,12 @@ # Getting a Wireguard Server + You can create your own wireguard server using a host service like DigitalOcean, or you can get a VPN service that provides WireGuard configs. I recommend ProtonVPN, because it is highly secure and has a great WireGuard config generator. -Simply go to https://account.protonvpn.com/downloads and scroll down to the +Simply go to and scroll down to the wireguard section to generate your configs, then paste into the appropriate section below. @@ -25,9 +26,11 @@ naming should also be similar (e.g. `/Users/jonny/Library/LaunchAgents/com.ProtonUS.adblock.plist`) ## Config File + Make sure you use a unique port for every separate server I recommend you set proxy authentication, you can use the same user/pass for all -``` + +```ini # Link to the Downloaded config WGConfig = /Users/jonny/vpntabs/ProtonUS.adblock.server.conf @@ -43,24 +46,27 @@ BindAddress = 127.0.0.1:25344 # Update the port here for each new server ``` ## Startup Script File + This is a bash script to facilitate startup, not strictly essential, but adds ease. Note, you MUST update the first path to wherever you installed this code to. Make sure you use the path for the config file above, not the one you downloaded from e.g. protonvpn. -``` + +```bash #!/bin/bash /Users/jonny/wireproxy/wireproxy -c /Users/jonny/vpntabs/ProtonUS.adblock.conf ``` ## MacOS LaunchAgent + To make it run every time you start your computer, you can create a launch agent in `$HOME/Library/LaunchAgents`. Name reference above. That file should contain the following, the label should be the same as the file name and the paths should be set correctly: -``` +```xml @@ -70,7 +76,7 @@ name and the paths should be set correctly: Program /Users/jonny/vpntabs/ProtonUS.adblock.sh RunAtLoad - + KeepAlive @@ -82,6 +88,7 @@ To enable it, run `launchtl start ~/Library/LaunchAgents/com.PortonUS.adblock.plist` # Firefox Setup + You will need to enable the Multi Account Container Tabs extension and a proxy extension, I recommend Sideberry, but Container Proxy also works. diff --git a/assets/iproyal.png b/assets/iproyal.png new file mode 100644 index 0000000..f48a845 Binary files /dev/null and b/assets/iproyal.png differ diff --git a/cmd/wireproxy/main.go b/cmd/wireproxy/main.go index 48880c2..713943a 100644 --- a/cmd/wireproxy/main.go +++ b/cmd/wireproxy/main.go @@ -22,6 +22,12 @@ import ( // an argument to denote that this process was spawned by -d const daemonProcess = "daemon-process" +// default paths for wireproxy config file +var default_config_paths = []string { + "/etc/wireproxy/wireproxy.conf", + os.Getenv("HOME")+"/.config/wireproxy.conf", +} + var version = "1.0.8-dev" func panicIfError(err error) { @@ -51,6 +57,16 @@ func executablePath() string { return programPath } +// check if default config file paths exist +func configFilePath() (string, bool) { + for _, path := range default_config_paths { + if _, err := os.Stat(path); err == nil { + return path, true + } + } + return "", false +} + func lock(stage string) { switch stage { case "boot": @@ -177,8 +193,12 @@ func main() { } if *config == "" { - fmt.Println("configuration path is required") - return + if path, config_exist := configFilePath(); config_exist { + *config = path + } else { + fmt.Println("configuration path is required") + return + } } if !*daemon { diff --git a/config.go b/config.go index 76593cf..1f6e4e4 100644 --- a/config.go +++ b/config.go @@ -5,6 +5,7 @@ import ( "encoding/hex" "errors" "net" + "os" "strings" "github.com/go-ini/ini" @@ -68,6 +69,18 @@ func parseString(section *ini.Section, keyName string) (string, error) { if key == nil { return "", errors.New(keyName + " should not be empty") } + value := key.String() + if strings.HasPrefix(value, "$") { + if strings.HasPrefix(value, "$$") { + return strings.Replace(value, "$$", "$", 1), nil + } + var ok bool + value, ok = os.LookupEnv(strings.TrimPrefix(value, "$")) + if !ok { + return "", errors.New(keyName + " references unset environment variable " + key.String()) + } + return value, nil + } return key.String(), nil } @@ -122,15 +135,21 @@ func encodeBase64ToHex(key string) (string, error) { } func parseNetIP(section *ini.Section, keyName string) ([]netip.Addr, error) { - key := section.Key(keyName) - if key == nil { - return []netip.Addr{}, nil + key, err := parseString(section, keyName) + if err != nil { + if strings.Contains(err.Error(), "should not be empty") { + return []netip.Addr{}, nil + } + return nil, err } - keys := key.StringsWithShadows(",") + keys := strings.Split(key, ",") var ips = make([]netip.Addr, 0, len(keys)) for _, str := range keys { str = strings.TrimSpace(str) + if len(str) == 0 { + continue + } ip, err := netip.ParseAddr(str) if err != nil { return nil, err @@ -141,34 +160,53 @@ func parseNetIP(section *ini.Section, keyName string) ([]netip.Addr, error) { } func parseCIDRNetIP(section *ini.Section, keyName string) ([]netip.Addr, error) { - key := section.Key(keyName) - if key == nil { - return []netip.Addr{}, nil + key, err := parseString(section, keyName) + if err != nil { + if strings.Contains(err.Error(), "should not be empty") { + return []netip.Addr{}, nil + } + return nil, err } - keys := key.StringsWithShadows(",") + keys := strings.Split(key, ",") var ips = make([]netip.Addr, 0, len(keys)) for _, str := range keys { - prefix, err := netip.ParsePrefix(str) - if err != nil { - return nil, err + str = strings.TrimSpace(str) + if len(str) == 0 { + continue + } + + if addr, err := netip.ParseAddr(str); err == nil { + ips = append(ips, addr) + } else { + prefix, err := netip.ParsePrefix(str) + if err != nil { + return nil, err + } + + addr := prefix.Addr() + ips = append(ips, addr) } - - addr := prefix.Addr() - ips = append(ips, addr) } return ips, nil } func parseAllowedIPs(section *ini.Section) ([]netip.Prefix, error) { - key := section.Key("AllowedIPs") - if key == nil { - return []netip.Prefix{}, nil + key, err := parseString(section, "AllowedIPs") + if err != nil { + if strings.Contains(err.Error(), "should not be empty") { + return []netip.Prefix{}, nil + } + return nil, err } - keys := key.StringsWithShadows(",") + keys := strings.Split(key, ",") var ips = make([]netip.Prefix, 0, len(keys)) for _, str := range keys { + str = strings.TrimSpace(str) + if len(str) == 0 { + continue + } prefix, err := netip.ParsePrefix(str) if err != nil { return nil, err diff --git a/config_test.go b/config_test.go new file mode 100644 index 0000000..948fbf8 --- /dev/null +++ b/config_test.go @@ -0,0 +1,87 @@ +package wireproxy + +import ( + "github.com/go-ini/ini" + "testing" +) + +func loadIniConfig(config string) (*ini.File, error) { + iniOpt := ini.LoadOptions{ + Insensitive: true, + AllowShadows: true, + AllowNonUniqueSections: true, + } + + return ini.LoadSources(iniOpt, []byte(config)) +} + +func TestWireguardConfWithoutSubnet(t *testing.T) { + const config = ` +[Interface] +PrivateKey = LAr1aNSNF9d0MjwUgAVC4020T0N/E5NUtqVv5EnsSz0= +Address = 10.5.0.2 +DNS = 1.1.1.1 + +[Peer] +PublicKey = e8LKAc+f9xEzq9Ar7+MfKRrs+gZ/4yzvpRJLRJ/VJ1w= +AllowedIPs = 0.0.0.0/0, ::/0 +Endpoint = 94.140.11.15:51820 +PersistentKeepalive = 25` + var cfg DeviceConfig + iniData, err := loadIniConfig(config) + if err != nil { + t.Fatal(err) + } + + err = ParseInterface(iniData, &cfg) + if err != nil { + t.Fatal(err) + } +} + +func TestWireguardConfWithSubnet(t *testing.T) { + const config = ` +[Interface] +PrivateKey = LAr1aNSNF9d0MjwUgAVC4020T0N/E5NUtqVv5EnsSz0= +Address = 10.5.0.2/23 +DNS = 1.1.1.1 + +[Peer] +PublicKey = e8LKAc+f9xEzq9Ar7+MfKRrs+gZ/4yzvpRJLRJ/VJ1w= +AllowedIPs = 0.0.0.0/0, ::/0 +Endpoint = 94.140.11.15:51820 +PersistentKeepalive = 25` + var cfg DeviceConfig + iniData, err := loadIniConfig(config) + if err != nil { + t.Fatal(err) + } + + err = ParseInterface(iniData, &cfg) + if err != nil { + t.Fatal(err) + } +} + +func TestWireguardConfWithManyAddress(t *testing.T) { + const config = ` +[Interface] +PrivateKey = mBsVDahr1XIu9PPd17UmsDdB6E53nvmS47NbNqQCiFM= +Address = 100.96.0.190,2606:B300:FFFF:fe8a:2ac6:c7e8:b021:6f5f/128 +DNS = 198.18.0.1,198.18.0.2 + +[Peer] +PublicKey = SHnh4C2aDXhp1gjIqceGhJrhOLSeNYcqWLKcYnzj00U= +AllowedIPs = 0.0.0.0/0,::/0 +Endpoint = 192.200.144.22:51820` + var cfg DeviceConfig + iniData, err := loadIniConfig(config) + if err != nil { + t.Fatal(err) + } + + err = ParseInterface(iniData, &cfg) + if err != nil { + t.Fatal(err) + } +} diff --git a/go.mod b/go.mod index 59ac71b..b022b0a 100644 --- a/go.mod +++ b/go.mod @@ -9,17 +9,16 @@ require ( github.com/akamensky/argparse v1.4.0 github.com/go-ini/ini v1.67.0 github.com/landlock-lsm/go-landlock v0.0.0-20240216195629-efb66220540a - github.com/sourcegraph/conc v0.3.0 github.com/things-go/go-socks5 v0.0.5 - golang.org/x/net v0.23.0 + golang.org/x/net v0.33.0 golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173 suah.dev/protect v1.2.3 ) require ( github.com/google/btree v1.1.2 // indirect - golang.org/x/crypto v0.21.0 // indirect - golang.org/x/sys v0.18.0 // indirect + golang.org/x/crypto v0.31.0 // indirect + golang.org/x/sys v0.28.0 // indirect golang.org/x/time v0.5.0 // indirect golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect gvisor.dev/gvisor v0.0.0-20230927004350-cbd86285d259 // indirect diff --git a/go.sum b/go.sum index 74abd33..f51522a 100644 --- a/go.sum +++ b/go.sum @@ -12,19 +12,17 @@ github.com/landlock-lsm/go-landlock v0.0.0-20240216195629-efb66220540a h1:dz+a1M github.com/landlock-lsm/go-landlock v0.0.0-20240216195629-efb66220540a/go.mod h1:1NY/VPO8xm3hXw3f+M65z+PJDLUaZA5cu7OfanxoUzY= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= -github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/things-go/go-socks5 v0.0.5 h1:qvKaGcBkfDrUL33SchHN93srAmYGzb4CxSM2DPYufe8= github.com/things-go/go-socks5 v0.0.5/go.mod h1:mtzInf8v5xmsBpHZVbIw2YQYhc4K0jRwzfsH64Uh0IQ= -golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA= -golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= -golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs= -golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= +golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= +golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= +golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I= +golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= -golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= +golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 h1:B82qJJgjvYKsXS9jeunTOisW56dUokqW/FOteYJJ/yg= diff --git a/http.go b/http.go index 9fa7932..88a7ef4 100644 --- a/http.go +++ b/http.go @@ -10,8 +10,6 @@ import ( "net" "net/http" "strings" - - "github.com/sourcegraph/conc" ) const proxyAuthHeaderKey = "Proxy-Authorization" @@ -31,23 +29,23 @@ func (s *HTTPServer) authenticate(req *http.Request) (int, error) { } 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") + if auth == "" { + return http.StatusProxyAuthRequired, fmt.Errorf("%s", http.StatusText(http.StatusProxyAuthRequired)) } - return http.StatusProxyAuthRequired, fmt.Errorf(http.StatusText(http.StatusProxyAuthRequired)) + 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") } func (s *HTTPServer) handleConn(req *http.Request, conn net.Conn) (peer net.Conn, err error) { @@ -103,7 +101,11 @@ func (s *HTTPServer) serve(conn net.Conn) { code, err := s.authenticate(req) if err != nil { - _ = responseWith(req, code).Write(conn) + resp := responseWith(req, code) + if code == http.StatusProxyAuthRequired { + resp.Header.Set("Proxy-Authenticate", "Basic realm=\"Proxy\"") + } + _ = resp.Write(conn) log.Println(err) return } @@ -127,17 +129,19 @@ func (s *HTTPServer) serve(conn net.Conn) { log.Println("dial proxy failed: peer nil") return } + go func() { - wg := conc.NewWaitGroup() - wg.Go(func() { - _, err = io.Copy(conn, peer) - _ = conn.Close() - }) - wg.Go(func() { - _, err = io.Copy(peer, conn) - _ = peer.Close() - }) - wg.Wait() + defer conn.Close() + defer peer.Close() + + _, _ = io.Copy(conn, peer) + }() + + go func() { + defer conn.Close() + defer peer.Close() + + _, _ = io.Copy(peer, conn) }() } diff --git a/rc.d/README.md b/rc.d/README.md new file mode 100644 index 0000000..41ee820 --- /dev/null +++ b/rc.d/README.md @@ -0,0 +1,21 @@ +# Running wireproxy with rc.d + +If you're on a rc.d-based distro, you'll most likely want to run Wireproxy as a systemd unit. + +The provided systemd unit assumes you have the wireproxy executable installed on `/bin/wireproxy` and a configuration file stored at `/etc/wireproxy.conf`. These paths can be customized by editing the unit file. + +# Setting up the unit + +1. Copy the `wireproxy` file from this directory to `/usr/local/etc/rc.d`. + +2. If necessary, customize the unit. + Edit the parts with `procname`, `command`, `wireproxy_conf` to point to the executable and the configuration file. + +4. Add the following lines to `/etc/rc.conf` to enable wireproxy + `wireproxy_enable="YES"` + +5. Start wireproxy service and check status + ``` + sudo service wireproxy start + sudo service wireproxy status + ``` diff --git a/rc.d/wireproxy b/rc.d/wireproxy new file mode 100644 index 0000000..47b8f2e --- /dev/null +++ b/rc.d/wireproxy @@ -0,0 +1,30 @@ +#!/bin/sh +# +# PROVIDE: wireproxy +# REQUIRE: DAEMON +# KEYWORD: nojail +# + +# +# Add the following lines to /etc/rc.conf to enable wireproxy: +# +#wireproxy_enable="YES" +# + +. /etc/rc.subr + +name=wireproxy +rcvar=wireproxy_enable + +load_rc_config $name +procname="/bin/wireproxy" + +wireproxy_enable=${wireproxy_enable:-"NO"} + +wireproxy_bin=/bin/wireproxy +wireproxy_conf=/etc/wireproxy.conf + +command=${wireproxy_bin} +command_args="-s -d -c ${wireproxy_conf}" + +run_rc_command "$1" diff --git a/routine.go b/routine.go index 465e6b1..edfc793 100644 --- a/routine.go +++ b/routine.go @@ -21,9 +21,9 @@ import ( "path" "strconv" "strings" + "sync" "time" - "github.com/sourcegraph/conc" "github.com/things-go/go-socks5" "github.com/things-go/go-socks5/bufferpool" @@ -48,7 +48,8 @@ type VirtualTun struct { SystemDNS bool Conf *DeviceConfig // PingRecord stores the last time an IP was pinged - PingRecord map[string]uint64 + PingRecord map[string]uint64 + PingRecordLock *sync.Mutex } // RoutineSpawner spawns a routine (e.g. socks5, tcp static routes) after the configuration is parsed @@ -188,6 +189,9 @@ func (c CredentialValidator) Valid(username, password string) bool { // connForward copy data from `from` to `to` func connForward(from io.ReadWriteCloser, to io.ReadWriteCloser) { + defer from.Close() + defer to.Close() + _, err := io.Copy(to, from) if err != nil { errorLogger.Printf("Cannot forward traffic: %s\n", err.Error()) @@ -210,20 +214,8 @@ func tcpClientForward(vt *VirtualTun, raddr *addressPort, conn net.Conn) { return } - go func() { - wg := conc.NewWaitGroup() - wg.Go(func() { - connForward(sconn, conn) - }) - wg.Go(func() { - connForward(conn, sconn) - }) - wg.Wait() - _ = sconn.Close() - _ = conn.Close() - sconn = nil - conn = nil - }() + go connForward(sconn, conn) + go connForward(conn, sconn) } // STDIOTcpForward starts a new connection via wireguard and forward traffic from `conn` @@ -248,18 +240,8 @@ func STDIOTcpForward(vt *VirtualTun, raddr *addressPort) { return } - go func() { - wg := conc.NewWaitGroup() - wg.Go(func() { - connForward(os.Stdin, sconn) - }) - wg.Go(func() { - connForward(sconn, stdout) - }) - wg.Wait() - _ = sconn.Close() - sconn = nil - }() + go connForward(os.Stdin, sconn) + go connForward(sconn, stdout) } // SpawnRoutine spawns a local TCP server which acts as a proxy to the specified target @@ -309,20 +291,9 @@ func tcpServerForward(vt *VirtualTun, raddr *addressPort, conn net.Conn) { return } - go func() { - gr := conc.NewWaitGroup() - gr.Go(func() { - connForward(sconn, conn) - }) - gr.Go(func() { - connForward(conn, sconn) - }) - gr.Wait() - _ = sconn.Close() - _ = conn.Close() - sconn = nil - conn = nil - }() + go connForward(sconn, conn) + go connForward(conn, sconn) + } // SpawnRoutine spawns a TCP server on wireguard which acts as a proxy to the specified target @@ -475,7 +446,9 @@ func (d VirtualTun) pingIPs() { } } + d.PingRecordLock.Lock() d.PingRecord[addr.String()] = uint64(time.Now().Unix()) + d.PingRecordLock.Unlock() defer socket.Close() }() diff --git a/systemd/README.md b/systemd/README.md index 33e8d00..b1a4ea0 100644 --- a/systemd/README.md +++ b/systemd/README.md @@ -8,7 +8,7 @@ The provided systemd unit assumes you have the wireproxy executable installed on 1. Copy the `wireproxy.service` file from this directory to `/etc/systemd/system/`, or use the following cURL command to download it: ```bash - sudo curl https://raw.githubusercontent.com/pufferffish/wireproxy/master/systemd/wireproxy.service > /etc/systemd/system/wireproxy.service + curl https://raw.githubusercontent.com/pufferffish/wireproxy/master/systemd/wireproxy.service | sudo tee /etc/systemd/system/wireproxy.service ``` 2. If necessary, customize the unit. diff --git a/systemd/wireproxy.service b/systemd/wireproxy.service index 832f813..0ecc551 100644 --- a/systemd/wireproxy.service +++ b/systemd/wireproxy.service @@ -40,7 +40,7 @@ RestrictAddressFamilies=AF_INET AF_INET6 AF_NETLINK RestrictNamespaces=true RestrictRealtime=true SystemCallArchitectures=native -SystemCallFilter=@system-service +SystemCallFilter=@system-service @sandbox [Install] WantedBy=multi-user.target diff --git a/wireguard.go b/wireguard.go index 31057ed..71a2960 100644 --- a/wireguard.go +++ b/wireguard.go @@ -3,6 +3,7 @@ package wireproxy import ( "bytes" "fmt" + "sync" "net/netip" @@ -14,14 +15,14 @@ import ( // DeviceSetting contains the parameters for setting up a tun interface type DeviceSetting struct { - ipcRequest string - dns []netip.Addr - deviceAddr []netip.Addr - mtu int + IpcRequest string + DNS []netip.Addr + DeviceAddr []netip.Addr + MTU int } -// serialize the config into an IPC request and DeviceSetting -func createIPCRequest(conf *DeviceConfig) (*DeviceSetting, error) { +// CreateIPCRequest serialize the config into an IPC request and DeviceSetting +func CreateIPCRequest(conf *DeviceConfig) (*DeviceSetting, error) { var request bytes.Buffer request.WriteString(fmt.Sprintf("private_key=%s\n", conf.SecretKey)) @@ -54,23 +55,23 @@ func createIPCRequest(conf *DeviceConfig) (*DeviceSetting, error) { } } - setting := &DeviceSetting{ipcRequest: request.String(), dns: conf.DNS, deviceAddr: conf.Endpoint, mtu: conf.MTU} + setting := &DeviceSetting{IpcRequest: request.String(), DNS: conf.DNS, DeviceAddr: conf.Endpoint, MTU: conf.MTU} return setting, nil } // StartWireguard creates a tun interface on netstack given a configuration func StartWireguard(conf *DeviceConfig, logLevel int) (*VirtualTun, error) { - setting, err := createIPCRequest(conf) + setting, err := CreateIPCRequest(conf) if err != nil { return nil, err } - tun, tnet, err := netstack.CreateNetTUN(setting.deviceAddr, setting.dns, setting.mtu) + tun, tnet, err := netstack.CreateNetTUN(setting.DeviceAddr, setting.DNS, setting.MTU) if err != nil { return nil, err } dev := device.NewDevice(tun, conn.NewDefaultBind(), device.NewLogger(logLevel, "")) - err = dev.IpcSet(setting.ipcRequest) + err = dev.IpcSet(setting.IpcRequest) if err != nil { return nil, err } @@ -81,10 +82,11 @@ func StartWireguard(conf *DeviceConfig, logLevel int) (*VirtualTun, error) { } return &VirtualTun{ - Tnet: tnet, - Dev: dev, - Conf: conf, - SystemDNS: len(setting.dns) == 0, - PingRecord: make(map[string]uint64), + Tnet: tnet, + Dev: dev, + Conf: conf, + SystemDNS: len(setting.DNS) == 0, + PingRecord: make(map[string]uint64), + PingRecordLock: new(sync.Mutex), }, nil }