8 Commits

Author SHA1 Message Date
dependabot[bot]
e749217090 Bump golang.org/x/net from 0.21.0 to 0.23.0 (#113)
Bumps [golang.org/x/net](https://github.com/golang/net) from 0.21.0 to 0.23.0.
- [Commits](https://github.com/golang/net/compare/v0.21.0...v0.23.0)

---
updated-dependencies:
- dependency-name: golang.org/x/net
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-19 15:21:34 +01:00
pufferfish
6ab7069908 fix landlock restriction while files do not exists (#114) 2024-04-19 15:15:09 +01:00
pufferfish
a6797166eb Limit wireproxy's permissions with landlock (#108)
* Limit wireproxy's permissions with landlock

* Show better debug message

* Fix crash when info is null

* Fix crash when landlock ABI is outdated

* remove /dev/std{in,out,err} from landlock restriction
2024-04-13 02:38:48 +01:00
pufferfish
eccf83a0cf Add health status endpoint (#107)
* implement metric endpoint

* implement ICMP ping

* fix linting

* fix IPv6 pings

* Add documentation for --info
2024-04-12 05:24:16 +01:00
pufferfish
54cedea2e4 Update LICENSE 2024-04-10 03:42:15 +01:00
pufferffish
bbde9cd266 bump version 2024-04-06 20:21:37 +01:00
J. Dekker
4f066d050a systemd: tight sandboxing (#103)
wireproxy needs very little permissions, we can restrict it to basically
nothing. DynamicUser means the system will generate a UID on demand
for service, also CAP_NET_BIND_SERVICE can be used to allow this user
to bind to a port < 1024 if desired. Also LoadCredential lets us read
a file with tight permissions i.e. root:root 0400 and pass it to only
wireproxy in an ephemeral and constrained manner.

Signed-off-by: J. Dekker <jdek@itanimul.li>
2024-03-18 16:42:03 +00:00
pufferfish
c710def46d Update README.md to mention Amnezia fork 2024-02-20 19:40:19 +00:00
10 changed files with 437 additions and 68 deletions

View File

@@ -1,4 +1,4 @@
Copyright (c) 2023 octeep <github@bandersnatch.anonaddy.com> Copyright (c) 2024 Wind Wong <me@windtfw.com>
Permission to use, copy, modify, and distribute this software for any Permission to use, copy, modify, and distribute this software for any
purpose with or without fee is hereby granted, provided that the above purpose with or without fee is hereby granted, provided that the above

View File

@@ -20,6 +20,9 @@ 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 wireproxy is completely isolated from my network interfaces, and I don't need root to configure
anything. 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).
# Feature # Feature
- TCP static routing for client and server - TCP static routing for client and server
- SOCKS5/HTTP proxy (currently only CONNECT is supported) - SOCKS5/HTTP proxy (currently only CONNECT is supported)
@@ -35,7 +38,8 @@ anything.
``` ```
usage: wireproxy [-h|--help] [-c|--config "<value>"] [-s|--silent] usage: wireproxy [-h|--help] [-c|--config "<value>"] [-s|--silent]
[-d|--daemon] [-v|--version] [-n|--configtest] [-d|--daemon] [-i|--info "<value>"] [-v|--version]
[-n|--configtest]
Userspace wireguard client for proxying Userspace wireguard client for proxying
@@ -45,9 +49,11 @@ Arguments:
-c --config Path of configuration file -c --config Path of configuration file
-s --silent Silent mode -s --silent Silent mode
-d --daemon Make wireproxy run in background -d --daemon Make wireproxy run in background
-i --info Specify the address and port for exposing health status
-v --version Print version -v --version Print version
-n --configtest Configtest mode. Only check the configuration file for -n --configtest Configtest mode. Only check the configuration file for
validity. validity.
``` ```
# Build instruction # Build instruction
@@ -185,6 +191,64 @@ PublicKey = YYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYY=
AllowedIPs = 10.254.254.100/32 AllowedIPs = 10.254.254.100/32
# Note there is no Endpoint defined here. # 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.
Currently two endpoints are implemented:
`/metrics`: Exposes information of the wireguard daemon, this provides the same information you would get with `wg show`. [This](https://www.wireguard.com/xplatform/#example-dialog) shows an example of what the response would look like.
`/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:
```
[Interface]
PrivateKey = censored
Address = 10.2.0.2/32
DNS = 10.2.0.1
CheckAlive = 1.1.1.1, 3.3.3.3
CheckAliveInterval = 3
[Peer]
PublicKey = censored
AllowedIPs = 0.0.0.0/0
Endpoint = 149.34.244.174:51820
[Socks5]
BindAddress = 127.0.0.1:25344
```
`/readyz` would respond with
```
< HTTP/1.1 503 Service Unavailable
< Date: Thu, 11 Apr 2024 00:54:59 GMT
< Content-Length: 35
< Content-Type: text/plain; charset=utf-8
<
{"1.1.1.1":1712796899,"3.3.3.3":0}
```
And for:
```
[Interface]
PrivateKey = censored
Address = 10.2.0.2/32
DNS = 10.2.0.1
CheckAlive = 1.1.1.1
```
`/readyz` would respond with
```
< HTTP/1.1 200 OK
< Date: Thu, 11 Apr 2024 00:56:21 GMT
< Content-Length: 23
< Content-Type: text/plain; charset=utf-8
<
{"1.1.1.1":1712796979}
```
If nothing is set for `CheckAlive`, an empty JSON object with 200 will be the response.
The peer which the ICMP ping packet is routed to depends on the `AllowedIPs` set for each peers.
# Stargazers over time # Stargazers over time
[![Stargazers over time](https://starchart.cc/octeep/wireproxy.svg)](https://starchart.cc/octeep/wireproxy) [![Stargazers over time](https://starchart.cc/octeep/wireproxy.svg)](https://starchart.cc/octeep/wireproxy)

View File

@@ -3,10 +3,14 @@ package main
import ( import (
"context" "context"
"fmt" "fmt"
"github.com/landlock-lsm/go-landlock/landlock"
"log" "log"
"net"
"net/http"
"os" "os"
"os/exec" "os/exec"
"os/signal" "os/signal"
"strconv"
"syscall" "syscall"
"github.com/akamensky/argparse" "github.com/akamensky/argparse"
@@ -18,24 +22,24 @@ import (
// an argument to denote that this process was spawned by -d // an argument to denote that this process was spawned by -d
const daemonProcess = "daemon-process" const daemonProcess = "daemon-process"
var version = "1.0.5-dev" var version = "1.0.8-dev"
// attempts to pledge and panic if it fails func panicIfError(err error) {
// this does nothing on non-OpenBSD systems
func pledgeOrPanic(promises string) {
err := protect.Pledge(promises)
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
} }
// attempts to pledge and panic if it fails
// this does nothing on non-OpenBSD systems
func pledgeOrPanic(promises string) {
panicIfError(protect.Pledge(promises))
}
// attempts to unveil and panic if it fails // attempts to unveil and panic if it fails
// this does nothing on non-OpenBSD systems // this does nothing on non-OpenBSD systems
func unveilOrPanic(path string, flags string) { func unveilOrPanic(path string, flags string) {
err := protect.Unveil(path, flags) panicIfError(protect.Unveil(path, flags))
if err != nil {
log.Fatal(err)
}
} }
// get the executable path via syscalls or infer it from argv // get the executable path via syscalls or infer it from argv
@@ -47,6 +51,91 @@ func executablePath() string {
return programPath return programPath
} }
func lock(stage string) {
switch stage {
case "boot":
exePath := executablePath()
// OpenBSD
unveilOrPanic("/", "r")
unveilOrPanic(exePath, "x")
// only allow standard stdio operation, file reading, networking, and exec
// also remove unveil permission to lock unveil
pledgeOrPanic("stdio rpath inet dns proc exec")
// Linux
panicIfError(landlock.V1.BestEffort().RestrictPaths(
landlock.RODirs("/"),
))
case "boot-daemon":
case "read-config":
// OpenBSD
pledgeOrPanic("stdio rpath inet dns")
case "ready":
// no file access is allowed from now on, only networking
// OpenBSD
pledgeOrPanic("stdio inet dns")
// Linux
net.DefaultResolver.PreferGo = true // needed to lock down dependencies
panicIfError(landlock.V1.BestEffort().RestrictPaths(
landlock.ROFiles("/etc/resolv.conf").IgnoreIfMissing(),
landlock.ROFiles("/dev/fd").IgnoreIfMissing(),
landlock.ROFiles("/dev/zero").IgnoreIfMissing(),
landlock.ROFiles("/dev/urandom").IgnoreIfMissing(),
landlock.ROFiles("/etc/localtime").IgnoreIfMissing(),
landlock.ROFiles("/proc/self/stat").IgnoreIfMissing(),
landlock.ROFiles("/proc/self/status").IgnoreIfMissing(),
landlock.ROFiles("/usr/share/locale").IgnoreIfMissing(),
landlock.ROFiles("/proc/self/cmdline").IgnoreIfMissing(),
landlock.ROFiles("/usr/share/zoneinfo").IgnoreIfMissing(),
landlock.ROFiles("/proc/sys/kernel/version").IgnoreIfMissing(),
landlock.ROFiles("/proc/sys/kernel/ngroups_max").IgnoreIfMissing(),
landlock.ROFiles("/proc/sys/kernel/cap_last_cap").IgnoreIfMissing(),
landlock.ROFiles("/proc/sys/vm/overcommit_memory").IgnoreIfMissing(),
landlock.RWFiles("/dev/log").IgnoreIfMissing(),
landlock.RWFiles("/dev/null").IgnoreIfMissing(),
landlock.RWFiles("/dev/full").IgnoreIfMissing(),
landlock.RWFiles("/proc/self/fd").IgnoreIfMissing(),
))
default:
panic("invalid stage")
}
}
func extractPort(addr string) uint16 {
_, portStr, err := net.SplitHostPort(addr)
if err != nil {
panic(fmt.Errorf("failed to extract port from %s: %w", addr, err))
}
port, err := strconv.Atoi(portStr)
if err != nil {
panic(fmt.Errorf("failed to extract port from %s: %w", addr, err))
}
return uint16(port)
}
func lockNetwork(sections []wireproxy.RoutineSpawner, infoAddr *string) {
var rules []landlock.Rule
if infoAddr != nil && *infoAddr != "" {
rules = append(rules, landlock.BindTCP(extractPort(*infoAddr)))
}
for _, section := range sections {
switch section := section.(type) {
case *wireproxy.TCPServerTunnelConfig:
rules = append(rules, landlock.ConnectTCP(extractPort(section.Target)))
case *wireproxy.HTTPConfig:
rules = append(rules, landlock.BindTCP(extractPort(section.BindAddress)))
case *wireproxy.TCPClientTunnelConfig:
rules = append(rules, landlock.ConnectTCP(uint16(section.BindAddress.Port)))
case *wireproxy.Socks5Config:
rules = append(rules, landlock.BindTCP(extractPort(section.BindAddress)))
}
}
panicIfError(landlock.V4.BestEffort().RestrictNet(rules...))
}
func main() { func main() {
s := make(chan os.Signal, 1) s := make(chan os.Signal, 1)
signal.Notify(s, syscall.SIGINT, syscall.SIGQUIT) signal.Notify(s, syscall.SIGINT, syscall.SIGQUIT)
@@ -58,18 +147,12 @@ func main() {
}() }()
exePath := executablePath() exePath := executablePath()
unveilOrPanic("/", "r") lock("boot")
unveilOrPanic(exePath, "x")
// only allow standard stdio operation, file reading, networking, and exec
// also remove unveil permission to lock unveil
pledgeOrPanic("stdio rpath inet dns proc exec")
isDaemonProcess := len(os.Args) > 1 && os.Args[1] == daemonProcess isDaemonProcess := len(os.Args) > 1 && os.Args[1] == daemonProcess
args := os.Args args := os.Args
if isDaemonProcess { if isDaemonProcess {
// remove proc and exec if they are not needed lock("boot-daemon")
pledgeOrPanic("stdio rpath inet dns")
args = []string{args[0]} args = []string{args[0]}
args = append(args, os.Args[2:]...) args = append(args, os.Args[2:]...)
} }
@@ -78,6 +161,7 @@ func main() {
config := parser.String("c", "config", &argparse.Options{Help: "Path of configuration file"}) config := parser.String("c", "config", &argparse.Options{Help: "Path of configuration file"})
silent := parser.Flag("s", "silent", &argparse.Options{Help: "Silent mode"}) silent := parser.Flag("s", "silent", &argparse.Options{Help: "Silent mode"})
daemon := parser.Flag("d", "daemon", &argparse.Options{Help: "Make wireproxy run in background"}) daemon := parser.Flag("d", "daemon", &argparse.Options{Help: "Make wireproxy run in background"})
info := parser.String("i", "info", &argparse.Options{Help: "Specify the address and port for exposing health status"})
printVerison := parser.Flag("v", "version", &argparse.Options{Help: "Print version"}) printVerison := parser.Flag("v", "version", &argparse.Options{Help: "Print version"})
configTest := parser.Flag("n", "configtest", &argparse.Options{Help: "Configtest mode. Only check the configuration file for validity."}) configTest := parser.Flag("n", "configtest", &argparse.Options{Help: "Configtest mode. Only check the configuration file for validity."})
@@ -98,8 +182,7 @@ func main() {
} }
if !*daemon { if !*daemon {
// remove proc and exec if they are not needed lock("read-config")
pledgeOrPanic("stdio rpath inet dns")
} }
conf, err := wireproxy.ParseConfig(*config) conf, err := wireproxy.ParseConfig(*config)
@@ -112,6 +195,8 @@ func main() {
return return
} }
lockNetwork(conf.Routines, info)
if isDaemonProcess { if isDaemonProcess {
os.Stdout, _ = os.Open(os.DevNull) os.Stdout, _ = os.Open(os.DevNull)
os.Stderr, _ = os.Open(os.DevNull) os.Stderr, _ = os.Open(os.DevNull)
@@ -137,16 +222,26 @@ func main() {
logLevel = device.LogLevelSilent logLevel = device.LogLevelSilent
} }
// no file access is allowed from now on, only networking lock("ready")
pledgeOrPanic("stdio inet dns")
tnet, err := wireproxy.StartWireguard(conf.Device, logLevel) tun, err := wireproxy.StartWireguard(conf.Device, logLevel)
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
for _, spawner := range conf.Routines { for _, spawner := range conf.Routines {
go spawner.SpawnRoutine(tnet) go spawner.SpawnRoutine(tun)
}
tun.StartPingIPs()
if *info != "" {
go func() {
err := http.ListenAndServe(*info, tun)
if err != nil {
panic(err)
}
}()
} }
<-ctx.Done() <-ctx.Done()

View File

@@ -22,12 +22,14 @@ type PeerConfig struct {
// DeviceConfig contains the information to initiate a wireguard connection // DeviceConfig contains the information to initiate a wireguard connection
type DeviceConfig struct { type DeviceConfig struct {
SecretKey string SecretKey string
Endpoint []netip.Addr Endpoint []netip.Addr
Peers []PeerConfig Peers []PeerConfig
DNS []netip.Addr DNS []netip.Addr
MTU int MTU int
ListenPort *int ListenPort *int
CheckAlive []netip.Addr
CheckAliveInterval int
} }
type TCPClientTunnelConfig struct { type TCPClientTunnelConfig struct {
@@ -237,6 +239,25 @@ func ParseInterface(cfg *ini.File, device *DeviceConfig) error {
device.ListenPort = &value device.ListenPort = &value
} }
checkAlive, err := parseNetIP(section, "CheckAlive")
if err != nil {
return err
}
device.CheckAlive = checkAlive
device.CheckAliveInterval = 5
if sectionKey, err := section.GetKey("CheckAliveInterval"); err == nil {
value, err := sectionKey.Int()
if err != nil {
return err
}
if len(checkAlive) == 0 {
return errors.New("CheckAliveInterval is only valid when CheckAlive is set")
}
device.CheckAliveInterval = value
}
return nil return nil
} }

10
go.mod
View File

@@ -8,18 +8,20 @@ require (
github.com/MakeNowJust/heredoc/v2 v2.0.1 github.com/MakeNowJust/heredoc/v2 v2.0.1
github.com/akamensky/argparse v1.4.0 github.com/akamensky/argparse v1.4.0
github.com/go-ini/ini v1.67.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 github.com/things-go/go-socks5 v0.0.5
golang.org/x/net v0.23.0
golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173 golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173
suah.dev/protect v1.2.3 suah.dev/protect v1.2.3
) )
require ( require (
github.com/google/btree v1.1.2 // indirect github.com/google/btree v1.1.2 // indirect
github.com/sourcegraph/conc v0.3.0 // indirect golang.org/x/crypto v0.21.0 // indirect
golang.org/x/crypto v0.19.0 // indirect golang.org/x/sys v0.18.0 // indirect
golang.org/x/net v0.21.0 // indirect
golang.org/x/sys v0.17.0 // indirect
golang.org/x/time v0.5.0 // indirect golang.org/x/time v0.5.0 // indirect
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect
gvisor.dev/gvisor v0.0.0-20230927004350-cbd86285d259 // indirect gvisor.dev/gvisor v0.0.0-20230927004350-cbd86285d259 // indirect
kernel.org/pub/linux/libs/security/libcap/psx v1.2.69 // indirect
) )

16
go.sum
View File

@@ -8,6 +8,8 @@ github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A=
github.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8= github.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8=
github.com/google/btree v1.1.2 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU= github.com/google/btree v1.1.2 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU=
github.com/google/btree v1.1.2/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= github.com/google/btree v1.1.2/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4=
github.com/landlock-lsm/go-landlock v0.0.0-20240216195629-efb66220540a h1:dz+a1MiMQksVhejeZwqJuzPawYQBwug74J8PPtkLl9U=
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 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 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 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
@@ -16,13 +18,13 @@ github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcU
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 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 h1:qvKaGcBkfDrUL33SchHN93srAmYGzb4CxSM2DPYufe8=
github.com/things-go/go-socks5 v0.0.5/go.mod h1:mtzInf8v5xmsBpHZVbIw2YQYhc4K0jRwzfsH64Uh0IQ= github.com/things-go/go-socks5 v0.0.5/go.mod h1:mtzInf8v5xmsBpHZVbIw2YQYhc4K0jRwzfsH64Uh0IQ=
golang.org/x/crypto v0.19.0 h1:ENy+Az/9Y1vSrlrvBSyna3PITt4tiZLf7sgCjZBX7Wo= golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA=
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4= golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y= golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.18.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 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= 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= golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 h1:B82qJJgjvYKsXS9jeunTOisW56dUokqW/FOteYJJ/yg=
@@ -33,5 +35,7 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gvisor.dev/gvisor v0.0.0-20230927004350-cbd86285d259 h1:TbRPT0HtzFP3Cno1zZo7yPzEEnfu8EjLfl6IU9VfqkQ= gvisor.dev/gvisor v0.0.0-20230927004350-cbd86285d259 h1:TbRPT0HtzFP3Cno1zZo7yPzEEnfu8EjLfl6IU9VfqkQ=
gvisor.dev/gvisor v0.0.0-20230927004350-cbd86285d259/go.mod h1:AVgIgHMwK63XvmAzWG9vLQ41YnVHN0du0tEC46fI7yY= gvisor.dev/gvisor v0.0.0-20230927004350-cbd86285d259/go.mod h1:AVgIgHMwK63XvmAzWG9vLQ41YnVHN0du0tEC46fI7yY=
kernel.org/pub/linux/libs/security/libcap/psx v1.2.69 h1:IdrOs1ZgwGw5CI+BH6GgVVlOt+LAXoPyh7enr8lfaXs=
kernel.org/pub/linux/libs/security/libcap/psx v1.2.69/go.mod h1:+l6Ee2F59XiJ2I6WR5ObpC1utCQJZ/VLsEbQCD8RG24=
suah.dev/protect v1.2.3 h1:aHeoNwZ9YPp64hrYaN0g0djNE1eRujgH63CrfRrUKdc= suah.dev/protect v1.2.3 h1:aHeoNwZ9YPp64hrYaN0g0djNE1eRujgH63CrfRrUKdc=
suah.dev/protect v1.2.3/go.mod h1:n1R3XIbsnryKX7C1PO88i5Wgo0v8OTXm9K9FIKt4rfs= suah.dev/protect v1.2.3/go.mod h1:n1R3XIbsnryKX7C1PO88i5Wgo0v8OTXm9K9FIKt4rfs=

View File

@@ -1,15 +1,27 @@
package wireproxy package wireproxy
import ( import (
"bytes"
"context" "context"
srand "crypto/rand"
"crypto/subtle" "crypto/subtle"
"encoding/binary"
"encoding/json"
"errors" "errors"
"golang.org/x/net/icmp"
"golang.org/x/net/ipv4"
"golang.org/x/net/ipv6"
"golang.zx2c4.com/wireguard/device"
"io" "io"
"log" "log"
"math/rand" "math/rand"
"net" "net"
"net/http"
"os" "os"
"path"
"strconv" "strconv"
"strings"
"time"
"github.com/sourcegraph/conc" "github.com/sourcegraph/conc"
"github.com/things-go/go-socks5" "github.com/things-go/go-socks5"
@@ -32,7 +44,11 @@ type CredentialValidator struct {
// VirtualTun stores a reference to netstack network and DNS configuration // VirtualTun stores a reference to netstack network and DNS configuration
type VirtualTun struct { type VirtualTun struct {
Tnet *netstack.Net Tnet *netstack.Net
Dev *device.Device
SystemDNS bool SystemDNS bool
Conf *DeviceConfig
// PingRecord stores the last time an IP was pinged
PingRecord map[string]uint64
} }
// RoutineSpawner spawns a routine (e.g. socks5, tcp static routes) after the configuration is parsed // RoutineSpawner spawns a routine (e.g. socks5, tcp static routes) after the configuration is parsed
@@ -148,16 +164,16 @@ func (config *Socks5Config) SpawnRoutine(vt *VirtualTun) {
// SpawnRoutine spawns a http server. // SpawnRoutine spawns a http server.
func (config *HTTPConfig) SpawnRoutine(vt *VirtualTun) { func (config *HTTPConfig) SpawnRoutine(vt *VirtualTun) {
http := &HTTPServer{ server := &HTTPServer{
config: config, config: config,
dial: vt.Tnet.Dial, dial: vt.Tnet.Dial,
auth: CredentialValidator{config.Username, config.Password}, auth: CredentialValidator{config.Username, config.Password},
} }
if config.Username != "" || config.Password != "" { if config.Username != "" || config.Password != "" {
http.authRequired = true server.authRequired = true
} }
if err := http.ListenAndServe("tcp", config.BindAddress); err != nil { if err := server.ListenAndServe("tcp", config.BindAddress); err != nil {
log.Fatal(err) log.Fatal(err)
} }
} }
@@ -330,3 +346,151 @@ func (conf *TCPServerTunnelConfig) SpawnRoutine(vt *VirtualTun) {
go tcpServerForward(vt, raddr, conn) go tcpServerForward(vt, raddr, conn)
} }
} }
func (d VirtualTun) ServeHTTP(w http.ResponseWriter, r *http.Request) {
log.Printf("Health metric request: %s\n", r.URL.Path)
switch path.Clean(r.URL.Path) {
case "/readyz":
body, err := json.Marshal(d.PingRecord)
if err != nil {
errorLogger.Printf("Failed to get device metrics: %s\n", err.Error())
w.WriteHeader(http.StatusInternalServerError)
return
}
status := http.StatusOK
for _, record := range d.PingRecord {
lastPong := time.Unix(int64(record), 0)
// +2 seconds to account for the time it takes to ping the IP
if time.Since(lastPong) > time.Duration(d.Conf.CheckAliveInterval+2)*time.Second {
status = http.StatusServiceUnavailable
break
}
}
w.WriteHeader(status)
_, _ = w.Write(body)
_, _ = w.Write([]byte("\n"))
case "/metrics":
get, err := d.Dev.IpcGet()
if err != nil {
errorLogger.Printf("Failed to get device metrics: %s\n", err.Error())
w.WriteHeader(http.StatusInternalServerError)
return
}
var buf bytes.Buffer
for _, peer := range strings.Split(get, "\n") {
pair := strings.SplitN(peer, "=", 2)
if len(pair) != 2 {
buf.WriteString(peer)
continue
}
if pair[0] == "private_key" || pair[0] == "preshared_key" {
pair[1] = "REDACTED"
}
buf.WriteString(pair[0])
buf.WriteString("=")
buf.WriteString(pair[1])
buf.WriteString("\n")
}
w.WriteHeader(http.StatusOK)
_, _ = w.Write(buf.Bytes())
default:
w.WriteHeader(http.StatusNotFound)
}
}
func (d VirtualTun) pingIPs() {
for _, addr := range d.Conf.CheckAlive {
socket, err := d.Tnet.Dial("ping", addr.String())
if err != nil {
errorLogger.Printf("Failed to ping %s: %s\n", addr, err.Error())
continue
}
data := make([]byte, 16)
_, _ = srand.Read(data)
requestPing := icmp.Echo{
Seq: rand.Intn(1 << 16),
Data: data,
}
var icmpBytes []byte
if addr.Is4() {
icmpBytes, _ = (&icmp.Message{Type: ipv4.ICMPTypeEcho, Code: 0, Body: &requestPing}).Marshal(nil)
} else if addr.Is6() {
icmpBytes, _ = (&icmp.Message{Type: ipv6.ICMPTypeEchoRequest, Code: 0, Body: &requestPing}).Marshal(nil)
} else {
errorLogger.Printf("Failed to ping %s: invalid address: %s\n", addr, addr.String())
continue
}
_ = socket.SetReadDeadline(time.Now().Add(time.Duration(d.Conf.CheckAliveInterval) * time.Second))
_, err = socket.Write(icmpBytes)
if err != nil {
errorLogger.Printf("Failed to ping %s: %s\n", addr, err.Error())
continue
}
addr := addr
go func() {
n, err := socket.Read(icmpBytes[:])
if err != nil {
errorLogger.Printf("Failed to read ping response from %s: %s\n", addr, err.Error())
return
}
replyPacket, err := icmp.ParseMessage(1, icmpBytes[:n])
if err != nil {
errorLogger.Printf("Failed to parse ping response from %s: %s\n", addr, err.Error())
return
}
if addr.Is4() {
replyPing, ok := replyPacket.Body.(*icmp.Echo)
if !ok {
errorLogger.Printf("Failed to parse ping response from %s: invalid reply type: %s\n", addr, replyPacket.Type)
return
}
if !bytes.Equal(replyPing.Data, requestPing.Data) || replyPing.Seq != requestPing.Seq {
errorLogger.Printf("Failed to parse ping response from %s: invalid ping reply: %v\n", addr, replyPing)
return
}
}
if addr.Is6() {
replyPing, ok := replyPacket.Body.(*icmp.RawBody)
if !ok {
errorLogger.Printf("Failed to parse ping response from %s: invalid reply type: %s\n", addr, replyPacket.Type)
return
}
seq := binary.BigEndian.Uint16(replyPing.Data[2:4])
pongBody := replyPing.Data[4:]
if !bytes.Equal(pongBody, requestPing.Data) || int(seq) != requestPing.Seq {
errorLogger.Printf("Failed to parse ping response from %s: invalid ping reply: %v\n", addr, replyPing)
return
}
}
d.PingRecord[addr.String()] = uint64(time.Now().Unix())
defer socket.Close()
}()
}
}
func (d VirtualTun) StartPingIPs() {
for _, addr := range d.Conf.CheckAlive {
d.PingRecord[addr.String()] = 0
}
go func() {
for {
d.pingIPs()
time.Sleep(time.Duration(d.Conf.CheckAliveInterval) * time.Second)
}
}()
}

View File

@@ -13,24 +13,12 @@ The provided systemd unit assumes you have the wireproxy executable installed on
2. If necessary, customize the unit. 2. If necessary, customize the unit.
Edit the parts with `ExecStartPre=` and `ExecStart=` to point to the executable and the configuration file. For example, if wireproxy is installed on `/usr/bin` and the configuration file is located in `/opt/myfiles/wireproxy.conf` do the following change: Edit the parts with `LoadCredential`, `ExecStartPre=` and `ExecStart=` to point to the executable and the configuration file. For example, if wireproxy is installed on `/usr/bin` and the configuration file is located in `/opt/myfiles/wireproxy.conf` do the following change:
```service ```service
ExecStartPre=/usr/bin/wireproxy -n -c /opt/myfiles/wireproxy.conf LoadCredential=conf:/opt/myfiles/wireproxy.conf
ExecStart=/usr/bin/wireproxy -c /opt/myfiles/wireproxy.conf ExecStartPre=/usr/bin/wireproxy -n -c ${CREDENTIALS_DIRECTORY}/conf
ExecStart=/usr/bin/wireproxy -c ${CREDENTIALS_DIRECTORY}/conf
``` ```
#### 2.2 Drop root privileges (optional, but recommended)
Without any modifications, this Wireproxy service will run as root. You might want to drop those privileges. One way to do this is to simply create a system account for Wireproxy (or just use your own user account to run it instead).
```bash
sudo useradd --comment "Wireproxy tunnel" --system wireproxy
```
Then uncomment these lines from the wireproxy.service:
```service
#User=wireproxy
#Group=wireproxy
```
Caveats:
1) Make sure `wireproxy` user can read the wireproxy configuration file.
2) Also note that unprivileged user cannot bind to ports below 1024 by default.
4. Reload systemd and enable the unit. 4. Reload systemd and enable the unit.
```bash ```bash

View File

@@ -4,15 +4,43 @@ Wants=network-online.target
After=network-online.target After=network-online.target
[Service] [Service]
#Uncomment and/or change these if you don't want to run Wireproxy as root User=wireproxy
#User=wireproxy Group=wireproxy
#Group=wireproxy SyslogIdentifier=wireproxy
Type=simple Type=simple
Restart=on-failure Restart=on-failure
RestartSec=30s RestartSec=30s
ExecStartPre=/opt/wireproxy/wireproxy -n -c /etc/wireproxy.conf
ExecStart=/opt/wireproxy/wireproxy -c /etc/wireproxy.conf DynamicUser=yes
SyslogIdentifier=wireproxy LoadCredential=conf:/etc/wireproxy.conf
ExecStartPre=/opt/wireproxy/wireproxy -n -c ${CREDENTIALS_DIRECTORY}/conf
ExecStart=/opt/wireproxy/wireproxy -c ${CREDENTIALS_DIRECTORY}/conf
# Required if <1024 port
#AmbientCapabilities=CAP_NET_BIND_SERVICE
#CapabilityBoundingSet=CAP_NET_BIND_SERVICE
LimitNPROC=64
LockPersonality=true
MemoryDenyWriteExecute=true
NoNewPrivileges=true
PrivateDevices=true
PrivateTmp=true
PrivateUsers=true
ProcSubset=pid
ProtectClock=true
ProtectControlGroups=true
ProtectHome=true
ProtectHostname=true
ProtectKernelLogs=true
ProtectKernelModules=true
ProtectKernelTunables=true
ProtectProc=invisible
ProtectSystem=strict
RestrictAddressFamilies=AF_INET AF_INET6 AF_NETLINK
RestrictNamespaces=true
RestrictRealtime=true
SystemCallArchitectures=native
SystemCallFilter=@system-service
[Install] [Install]
WantedBy=multi-user.target WantedBy=multi-user.target

View File

@@ -81,7 +81,10 @@ func StartWireguard(conf *DeviceConfig, logLevel int) (*VirtualTun, error) {
} }
return &VirtualTun{ return &VirtualTun{
Tnet: tnet, Tnet: tnet,
SystemDNS: len(setting.dns) == 0, Dev: dev,
Conf: conf,
SystemDNS: len(setting.dns) == 0,
PingRecord: make(map[string]uint64),
}, nil }, nil
} }