Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e749217090 | ||
|
|
6ab7069908 | ||
|
|
a6797166eb | ||
|
|
eccf83a0cf | ||
|
|
54cedea2e4 | ||
|
|
bbde9cd266 | ||
|
|
4f066d050a | ||
|
|
c710def46d |
2
LICENSE
2
LICENSE
@@ -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
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
|
||||
66
README.md
66
README.md
@@ -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
|
||||
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
|
||||
- TCP static routing for client and server
|
||||
- SOCKS5/HTTP proxy (currently only CONNECT is supported)
|
||||
@@ -35,7 +38,8 @@ anything.
|
||||
|
||||
```
|
||||
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
|
||||
|
||||
@@ -45,9 +49,11 @@ Arguments:
|
||||
-c --config Path of configuration file
|
||||
-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
|
||||
@@ -185,6 +191,64 @@ 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.
|
||||
|
||||
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
|
||||
[](https://starchart.cc/octeep/wireproxy)
|
||||
|
||||
@@ -3,10 +3,14 @@ package main
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/landlock-lsm/go-landlock/landlock"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
"os/signal"
|
||||
"strconv"
|
||||
"syscall"
|
||||
|
||||
"github.com/akamensky/argparse"
|
||||
@@ -18,24 +22,24 @@ import (
|
||||
// an argument to denote that this process was spawned by -d
|
||||
const daemonProcess = "daemon-process"
|
||||
|
||||
var version = "1.0.5-dev"
|
||||
var version = "1.0.8-dev"
|
||||
|
||||
// attempts to pledge and panic if it fails
|
||||
// this does nothing on non-OpenBSD systems
|
||||
func pledgeOrPanic(promises string) {
|
||||
err := protect.Pledge(promises)
|
||||
func panicIfError(err error) {
|
||||
if err != nil {
|
||||
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
|
||||
// this does nothing on non-OpenBSD systems
|
||||
func unveilOrPanic(path string, flags string) {
|
||||
err := protect.Unveil(path, flags)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
panicIfError(protect.Unveil(path, flags))
|
||||
}
|
||||
|
||||
// get the executable path via syscalls or infer it from argv
|
||||
@@ -47,6 +51,91 @@ func executablePath() string {
|
||||
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() {
|
||||
s := make(chan os.Signal, 1)
|
||||
signal.Notify(s, syscall.SIGINT, syscall.SIGQUIT)
|
||||
@@ -58,18 +147,12 @@ func main() {
|
||||
}()
|
||||
|
||||
exePath := executablePath()
|
||||
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")
|
||||
lock("boot")
|
||||
|
||||
isDaemonProcess := len(os.Args) > 1 && os.Args[1] == daemonProcess
|
||||
args := os.Args
|
||||
if isDaemonProcess {
|
||||
// remove proc and exec if they are not needed
|
||||
pledgeOrPanic("stdio rpath inet dns")
|
||||
lock("boot-daemon")
|
||||
args = []string{args[0]}
|
||||
args = append(args, os.Args[2:]...)
|
||||
}
|
||||
@@ -78,6 +161,7 @@ func main() {
|
||||
config := parser.String("c", "config", &argparse.Options{Help: "Path of configuration file"})
|
||||
silent := parser.Flag("s", "silent", &argparse.Options{Help: "Silent mode"})
|
||||
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"})
|
||||
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 {
|
||||
// remove proc and exec if they are not needed
|
||||
pledgeOrPanic("stdio rpath inet dns")
|
||||
lock("read-config")
|
||||
}
|
||||
|
||||
conf, err := wireproxy.ParseConfig(*config)
|
||||
@@ -112,6 +195,8 @@ func main() {
|
||||
return
|
||||
}
|
||||
|
||||
lockNetwork(conf.Routines, info)
|
||||
|
||||
if isDaemonProcess {
|
||||
os.Stdout, _ = os.Open(os.DevNull)
|
||||
os.Stderr, _ = os.Open(os.DevNull)
|
||||
@@ -137,16 +222,26 @@ func main() {
|
||||
logLevel = device.LogLevelSilent
|
||||
}
|
||||
|
||||
// no file access is allowed from now on, only networking
|
||||
pledgeOrPanic("stdio inet dns")
|
||||
lock("ready")
|
||||
|
||||
tnet, err := wireproxy.StartWireguard(conf.Device, logLevel)
|
||||
tun, err := wireproxy.StartWireguard(conf.Device, logLevel)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
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()
|
||||
|
||||
21
config.go
21
config.go
@@ -28,6 +28,8 @@ type DeviceConfig struct {
|
||||
DNS []netip.Addr
|
||||
MTU int
|
||||
ListenPort *int
|
||||
CheckAlive []netip.Addr
|
||||
CheckAliveInterval int
|
||||
}
|
||||
|
||||
type TCPClientTunnelConfig struct {
|
||||
@@ -237,6 +239,25 @@ func ParseInterface(cfg *ini.File, device *DeviceConfig) error {
|
||||
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
|
||||
}
|
||||
|
||||
|
||||
10
go.mod
10
go.mod
@@ -8,18 +8,20 @@ require (
|
||||
github.com/MakeNowJust/heredoc/v2 v2.0.1
|
||||
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.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173
|
||||
suah.dev/protect v1.2.3
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/google/btree v1.1.2 // indirect
|
||||
github.com/sourcegraph/conc v0.3.0 // indirect
|
||||
golang.org/x/crypto v0.19.0 // indirect
|
||||
golang.org/x/net v0.21.0 // indirect
|
||||
golang.org/x/sys v0.17.0 // indirect
|
||||
golang.org/x/crypto v0.21.0 // indirect
|
||||
golang.org/x/sys v0.18.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
|
||||
kernel.org/pub/linux/libs/security/libcap/psx v1.2.69 // indirect
|
||||
)
|
||||
|
||||
16
go.sum
16
go.sum
@@ -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/google/btree v1.1.2 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU=
|
||||
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/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
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/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.19.0 h1:ENy+Az/9Y1vSrlrvBSyna3PITt4tiZLf7sgCjZBX7Wo=
|
||||
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
||||
golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4=
|
||||
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
||||
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/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.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
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/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=
|
||||
@@ -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=
|
||||
gvisor.dev/gvisor v0.0.0-20230927004350-cbd86285d259 h1:TbRPT0HtzFP3Cno1zZo7yPzEEnfu8EjLfl6IU9VfqkQ=
|
||||
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/go.mod h1:n1R3XIbsnryKX7C1PO88i5Wgo0v8OTXm9K9FIKt4rfs=
|
||||
|
||||
170
routine.go
170
routine.go
@@ -1,15 +1,27 @@
|
||||
package wireproxy
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
srand "crypto/rand"
|
||||
"crypto/subtle"
|
||||
"encoding/binary"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"golang.org/x/net/icmp"
|
||||
"golang.org/x/net/ipv4"
|
||||
"golang.org/x/net/ipv6"
|
||||
"golang.zx2c4.com/wireguard/device"
|
||||
"io"
|
||||
"log"
|
||||
"math/rand"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"path"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/sourcegraph/conc"
|
||||
"github.com/things-go/go-socks5"
|
||||
@@ -32,7 +44,11 @@ type CredentialValidator struct {
|
||||
// VirtualTun stores a reference to netstack network and DNS configuration
|
||||
type VirtualTun struct {
|
||||
Tnet *netstack.Net
|
||||
Dev *device.Device
|
||||
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
|
||||
@@ -148,16 +164,16 @@ func (config *Socks5Config) SpawnRoutine(vt *VirtualTun) {
|
||||
|
||||
// SpawnRoutine spawns a http server.
|
||||
func (config *HTTPConfig) SpawnRoutine(vt *VirtualTun) {
|
||||
http := &HTTPServer{
|
||||
server := &HTTPServer{
|
||||
config: config,
|
||||
dial: vt.Tnet.Dial,
|
||||
auth: CredentialValidator{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)
|
||||
}
|
||||
}
|
||||
@@ -330,3 +346,151 @@ func (conf *TCPServerTunnelConfig) SpawnRoutine(vt *VirtualTun) {
|
||||
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)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
@@ -13,24 +13,12 @@ The provided systemd unit assumes you have the wireproxy executable installed on
|
||||
|
||||
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
|
||||
ExecStartPre=/usr/bin/wireproxy -n -c /opt/myfiles/wireproxy.conf
|
||||
ExecStart=/usr/bin/wireproxy -c /opt/myfiles/wireproxy.conf
|
||||
LoadCredential=conf:/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.
|
||||
```bash
|
||||
|
||||
@@ -4,15 +4,43 @@ Wants=network-online.target
|
||||
After=network-online.target
|
||||
|
||||
[Service]
|
||||
#Uncomment and/or change these if you don't want to run Wireproxy as root
|
||||
#User=wireproxy
|
||||
#Group=wireproxy
|
||||
User=wireproxy
|
||||
Group=wireproxy
|
||||
SyslogIdentifier=wireproxy
|
||||
Type=simple
|
||||
Restart=on-failure
|
||||
RestartSec=30s
|
||||
ExecStartPre=/opt/wireproxy/wireproxy -n -c /etc/wireproxy.conf
|
||||
ExecStart=/opt/wireproxy/wireproxy -c /etc/wireproxy.conf
|
||||
SyslogIdentifier=wireproxy
|
||||
|
||||
DynamicUser=yes
|
||||
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]
|
||||
WantedBy=multi-user.target
|
||||
|
||||
@@ -82,6 +82,9 @@ 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),
|
||||
}, nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user