diff options
| -rw-r--r-- | CHANGES.md | 3 | ||||
| -rw-r--r-- | docs/md/apidocs.md | 57 | ||||
| -rw-r--r-- | exports/exports.go | 107 | ||||
| -rw-r--r-- | exports/exports.h | 5 | ||||
| -rw-r--r-- | go.mod | 1 | ||||
| -rw-r--r-- | go.sum | 2 | ||||
| -rw-r--r-- | internal/wireguard/ini/ini.go | 2 | ||||
| -rw-r--r-- | proxy/proxy.go | 84 | ||||
| -rw-r--r-- | wrappers/python/eduvpn_common/loader.py | 31 | ||||
| -rw-r--r-- | wrappers/python/eduvpn_common/main.py | 44 | ||||
| -rw-r--r-- | wrappers/python/eduvpn_common/types.py | 1 |
11 files changed, 333 insertions, 4 deletions
@@ -3,8 +3,7 @@ - Escape changelog newlines * Lint: - lint use staticcheck instead of stylecheck with golangci-lint -* ProxyGuard functionality removed, this is implemented inside of the eduVPN clients separately either using a wireguard fork or a daemon in case of linux - - Removed function: `NewProxyguard`, `ProxyguardTunnel`, `ProxyguardRestart`, `ProxyguardPeerIPs` +* API: - The `Configuration` type no longer returns proxy information, in case of ProxyGuard the `ProxyEndpoint` in the INI config is filled in as returned by the eduVPN server # 3.0.0 (2025-03-21) diff --git a/docs/md/apidocs.md b/docs/md/apidocs.md index 71713e6..2769a27 100644 --- a/docs/md/apidocs.md +++ b/docs/md/apidocs.md @@ -605,6 +605,63 @@ Example Input: ```InState(5)``` Example Output: ```1, null``` +## NewProxyguard +Signature: + +```go +func NewProxyguard(c C.uintptr_t, lp C.int, tcpsp C.int, peer *C.char, proxySetup C.ProxySetup) (C.uintptr_t, *C.char) +``` + +NewProxyguard creates the 'proxyguard' procedure in eduvpn-common. If the +proxy cannot be created it returns an error. + +This function proxies WireGuard UDP connections over HTTP: [ProxyGuard on +Codeberg](https://codeberg.org/eduvpn/proxyguard). + +These input variables can be gotten from the configuration that is retrieved +using the `proxy` JSON key + + - `c` is the cookie. Note that if you cancel/delete the cookie, + ProxyGuard gets cleaned up. Common automatically cleans up ProxyGuard + when `Cleanup` is called, but it is good to cleanup yourself too. + - `lp` is the `port` of the local udp ProxyGuard connection, this is what + is set to the WireGuard endpoint + - `tcpsp` is the TCP source port. Pass 0 if you do not route based on + source port, so far only the Linux client has to pass non-zero. + - `peer` is the `ip:port` of the remote server + - `proxySetup` is a callback which is called when the socket is setting + up, this can be used for configuring routing in the client. It takes + two arguments: the file descriptor (integer) and a JSON list of IPs the + client connects to + +Example Input: ```NewProxyguard(myCookie, 1337, 0, "5.5.5.5:51820", +proxySetupCB)``` + +Example Output: ```null``` + +## ProxyguardPeerIPs +Signature: + +```go +func ProxyguardPeerIPs(proxyH C.uintptr_t) (*C.char, *C.char) +``` + +ProxyguardPeerIPs gets the Peer IPs configured by ProxyGuard Example Input: +```ProxyguardPeerIPs(handle)``` + +Example Output: ```["1.1.1.1"], null``` + +## ProxyguardTunnel +Signature: + +```go +func ProxyguardTunnel(c C.uintptr_t, proxyH C.uintptr_t, wglisten C.int) *C.char +``` + +ProxyguardTunnel starts the tunneling for ProxyGuard `c` is the cookie +`proxyH` is the proxy handle `wglisten` is the port WireGuard is listening +on + ## Register Signature: diff --git a/exports/exports.go b/exports/exports.go index a20ffb8..3ef6781 100644 --- a/exports/exports.go +++ b/exports/exports.go @@ -27,6 +27,7 @@ import ( "codeberg.org/eduVPN/eduvpn-common/client" "codeberg.org/eduVPN/eduvpn-common/i18n/err" + "codeberg.org/eduVPN/eduvpn-common/proxy" "codeberg.org/eduVPN/eduvpn-common/types/cookie" errtypes "codeberg.org/eduVPN/eduvpn-common/types/error" srvtypes "codeberg.org/eduVPN/eduvpn-common/types/server" @@ -876,6 +877,112 @@ func StartFailover(c C.uintptr_t, gateway *C.char, mtu C.int, readRxBytes C.Read return droppedC, nil } +// NewProxyguard creates the 'proxyguard' procedure in eduvpn-common. +// If the proxy cannot be created it returns an error. +// +// This function proxies WireGuard UDP connections over HTTP: [ProxyGuard on Codeberg](https://codeberg.org/eduvpn/proxyguard). +// +// These input variables can be gotten from the configuration that is retrieved using the `proxy` JSON key +// +// - `c` is the cookie. Note that if you cancel/delete the cookie, ProxyGuard gets cleaned up. Common automatically cleans up ProxyGuard when `Cleanup` is called, but it is good to cleanup yourself too. +// - `lp` is the `port` of the local udp ProxyGuard connection, this is what is set to the WireGuard endpoint +// - `tcpsp` is the TCP source port. Pass 0 if you do not route based on source port, so far only the Linux client has to pass non-zero. +// - `peer` is the `ip:port` of the remote server +// - `proxySetup` is a callback which is called when the socket is setting up, this can be used for configuring routing in the client. It takes two arguments: the file descriptor (integer) and a JSON list of IPs the client connects to +// +// Example Input: ```NewProxyguard(myCookie, 1337, 0, "5.5.5.5:51820", proxySetupCB)``` +// +// Example Output: ```null``` +// +//export NewProxyguard +func NewProxyguard(c C.uintptr_t, lp C.int, tcpsp C.int, peer *C.char, proxySetup C.ProxySetup) (C.uintptr_t, *C.char) { + ck, err := getCookie(c) + if err != nil { + return 0, getCError(err) + } + proxy, proxyErr := proxy.NewProxyguard(ck.Context(), int(lp), int(tcpsp), C.GoString(peer), func(fd int) { + if proxySetup == nil { + return + } + C.call_proxy_setup(proxySetup, C.int(fd)) + }) + if proxyErr != nil { + return 0, getCError(proxyErr) + } + return C.uintptr_t(cgo.NewHandle(proxy)), nil +} + +// ProxyguardRestart restarts ProxyGuard, call this when a network change happens +// +// Example Input: ```ProxyguardRestart(proxyHandle)``` +// +// Example Output: ```"failed restarting ProxyGuard"``` +// +//export ProxyguardRestart +func ProxyguardRestart(proxyH C.uintptr_t) *C.char { + pr, err := getProxy(proxyH) + if err != nil { + return getCError(err) + } + pr.Restart() + return nil +} + +func getProxy(proxyH C.uintptr_t) (*proxy.Proxy, error) { + h := cgo.Handle(proxyH) + v, ok := h.Value().(*proxy.Proxy) + if !ok { + return nil, i18nerr.NewInternal("value is not a proxyguard wrapper") + } + return v, nil +} + +// ProxyguardTunnel starts the tunneling for ProxyGuard +// `c` is the cookie +// `proxyH` is the proxy handle +// `wglisten` is the port WireGuard is listening on +// +//export ProxyguardTunnel +func ProxyguardTunnel(c C.uintptr_t, proxyH C.uintptr_t, wglisten C.int) *C.char { + ck, err := getCookie(c) + if err != nil { + return getCError(err) + } + pr, err := getProxy(proxyH) + if err != nil { + return getCError(err) + } + tunnelErr := pr.Tunnel(ck.Context(), int(wglisten)) + + // after tunneling is done, the handle should be deleted + cgo.Handle(proxyH).Delete() + return getCError(tunnelErr) +} + +// ProxyguardPeerIPs gets the Peer IPs configured by ProxyGuard +// Example Input: ```ProxyguardPeerIPs(handle)``` +// +// Example Output: ```["1.1.1.1"], null``` +// +//export ProxyguardPeerIPs +func ProxyguardPeerIPs(proxyH C.uintptr_t) (*C.char, *C.char) { + pr, err := getProxy(proxyH) + if err != nil { + return nil, getCError(err) + } + pips := pr.PeerIPS + + b, err := json.Marshal(pips) + if err != nil { + return nil, getCError(i18nerr.WrapInternal(err, "failed converting Peer IPs to JSON")) + } + ret, err := getReturnData(string(b)) + if err != nil { + return nil, getCError(err) + } + return C.CString(ret), nil +} + // SetState sets the state of the state machine. // // Note: this transitions the FSM into the new state without passing any data to it. diff --git a/exports/exports.h b/exports/exports.h index 13630a6..9ec39e4 100644 --- a/exports/exports.h +++ b/exports/exports.h @@ -11,6 +11,7 @@ typedef int (*StateCB)(int oldstate, int newstate, void* data); typedef void (*RefreshList)(); typedef void (*TokenGetter)(const char* server_id, int server_type, char* out, size_t len); typedef void (*TokenSetter)(const char* server_id, int server_type, const char* tokens); +typedef void (*ProxySetup)(int fd); static long long int get_read_rx_bytes(ReadRxBytes read) { @@ -32,5 +33,9 @@ static void call_token_setter(TokenSetter setter, const char* server_id, int ser { setter(server_id, server_type, tokens); } +static void call_proxy_setup(ProxySetup proxysetup, int fd) +{ + proxysetup(fd); +} #endif /* EXPORTS_H */ @@ -3,6 +3,7 @@ module codeberg.org/eduVPN/eduvpn-common go 1.23.4 require ( + codeberg.org/eduVPN/proxyguard v0.0.0-20250814100601-abc5db189743 github.com/jedisct1/go-minisign v0.0.0-20241212093149-d2f9f49435c7 github.com/jwijenbergh/eduoauth-go v1.1.2 github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c @@ -1,3 +1,5 @@ +codeberg.org/eduVPN/proxyguard v0.0.0-20250814100601-abc5db189743 h1:orR4wKonLoCeKWQz3mk268ZuYOKwrIu/pdSA5Wz44Zg= +codeberg.org/eduVPN/proxyguard v0.0.0-20250814100601-abc5db189743/go.mod h1:fc7DsdgdLmrO7DN45HNp+ekVewlRcikSOkAvUeGUvWk= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/jedisct1/go-minisign v0.0.0-20241212093149-d2f9f49435c7 h1:FWpSWRD8FbVkKQu8M1DM9jF5oXFLyE+XpisIYfdzbic= diff --git a/internal/wireguard/ini/ini.go b/internal/wireguard/ini/ini.go index 46c6f8b..c7eb971 100644 --- a/internal/wireguard/ini/ini.go +++ b/internal/wireguard/ini/ini.go @@ -53,7 +53,7 @@ type Section struct { keys OrderedKeys } -// KeyValue gets a value for key `key` +// keyValue gets a value for key `key` // It returns an error if the key does not exist func (sec *Section) keyValue(key string) (string, error) { if v, ok := sec.keyValues[key]; ok { diff --git a/proxy/proxy.go b/proxy/proxy.go new file mode 100644 index 0000000..af0cd89 --- /dev/null +++ b/proxy/proxy.go @@ -0,0 +1,84 @@ +// Package proxy is a wrapper around proxyguard that integrates it with eduvpn-common settings +// - leaves out some options not applicable to the common integration, e.g. fwmark +// - integrates with eduvpn-common's logger +// - integrates eduvpn-common's user agent +package proxy + +import ( + "context" + "fmt" + "log/slog" + + "codeberg.org/eduVPN/proxyguard" + + "codeberg.org/eduVPN/eduvpn-common/i18n/err" + httpw "codeberg.org/eduVPN/eduvpn-common/internal/http" +) + +// Logger is defined here such that we can update the proxyguard logger +type Logger struct{} + +// Logf logs a message with parameters +func (l *Logger) Logf(msg string, params ...any) { + slog.Info("Proxyguard log", "msg", fmt.Sprintf(msg, params...)) +} + +// Log logs a message +func (l *Logger) Log(msg string) { + slog.Info("Proxyguard log", "msg", msg) +} + +// Proxy is the ProxyGuard client with a channel used for restarting +type Proxy struct { + proxyguard.Client + resChan chan struct{} +} + +// NewProxyguard sets up proxyguard for proxied WireGuard connections +func NewProxyguard(ctx context.Context, lp int, tcpsp int, peer string, setupSocket func(fd int)) (*Proxy, error) { + proxyguard.UpdateLogger(&Logger{}) + proxy := Proxy{ + Client: proxyguard.Client{ + Peer: peer, + ListenPort: lp, + TCPSourcePort: tcpsp, + SetupSocket: setupSocket, + UserAgent: httpw.UserAgent, + }, + resChan: make(chan struct{}), + } + _, err := proxy.Setup(ctx) + if err != nil { + return nil, i18nerr.WrapInternal(err, "The ProxyGuard DNS could not be resolved") + } + + return &proxy, nil +} + +// Tunnel tunnels the ProxyGuard connection. `wglisten` is the WireGuard listen port +func (p *Proxy) Tunnel(ctx context.Context, wglisten int) error { + errChan := make(chan error, 1) + gctx, cancel := context.WithCancel(ctx) + go func() { + err := p.Client.Tunnel(gctx, wglisten) + if err != nil { + err = i18nerr.WrapInternal(err, "The VPN proxy exited") + } + errChan <- err + }() + + select { + case err := <-errChan: + cancel() + return err + case <-p.resChan: + cancel() + <-errChan + return p.Tunnel(ctx, wglisten) + } +} + +// Restart restarts the existing ProxyGuard process, for e.g. roaming +func (p *Proxy) Restart() { + p.resChan <- struct{}{} +} diff --git a/wrappers/python/eduvpn_common/loader.py b/wrappers/python/eduvpn_common/loader.py index 888b53f..d902453 100644 --- a/wrappers/python/eduvpn_common/loader.py +++ b/wrappers/python/eduvpn_common/loader.py @@ -6,6 +6,7 @@ from eduvpn_common.types import ( BoolError, DataError, HandlerError, + ProxySetup, ReadRxBytes, RefreshList, TokenGetter, @@ -131,3 +132,33 @@ def initialize_functions(lib: CDLL) -> None: ], BoolError, ) + lib.NewProxyguard.argtypes, lib.NewProxyguard.restype = ( + [ + c_int, + c_int, + c_int, + c_char_p, + ProxySetup, + ], + HandlerError, + ) + lib.ProxyguardRestart.argtypes, lib.ProxyguardRestart.restype = ( + [ + c_int, + ], + c_char_p, + ) + lib.ProxyguardTunnel.argtypes, lib.ProxyguardTunnel.restype = ( + [ + c_int, + c_int, + c_int, + ], + c_char_p, + ) + lib.ProxyguardPeerIPs.argtypes, lib.ProxyguardPeerIPs.restype = ( + [ + c_int, + ], + DataError, + ) diff --git a/wrappers/python/eduvpn_common/main.py b/wrappers/python/eduvpn_common/main.py index 2bd221d..e63ea92 100644 --- a/wrappers/python/eduvpn_common/main.py +++ b/wrappers/python/eduvpn_common/main.py @@ -1,12 +1,13 @@ import ctypes import json from enum import IntEnum -from typing import Any, Callable, Iterator, Optional +from typing import Any, Callable, Iterator, List, Optional from eduvpn_common.event import EventHandler from eduvpn_common.loader import initialize_functions, load_lib from eduvpn_common.state import State from eduvpn_common.types import ( + ProxySetup, ReadRxBytes, RefreshList, TokenGetter, @@ -19,6 +20,29 @@ from eduvpn_common.types import ( global_object = None +class Proxyguard(object): + def __init__(self, parent, handler): + self.parent = parent + self.handler = handler + + def tunnel(self, wglisten: int): + tunnel_err = self.parent.go_cookie_function(self.parent.lib.ProxyguardTunnel, self.handler, wglisten) + if tunnel_err: + forwardError(tunnel_err) + + @property + def peer_ips(self) -> List[str]: + peer_ips, peer_ips_err = self.parent.go_function(self.parent.lib.ProxyguardPeerIPs, self.handler) + if peer_ips_err: + forwardError(peer_ips_err) + return json.loads(peer_ips) + + def restart(self): + restart_err = self.parent.go_function(self.parent.lib.ProxyguardRestart, self.handler) + if restart_err: + forwardError(restart_err) + + class WrappedError(Exception): def __init__(self, translations, language, misc): self.translations = translations @@ -350,6 +374,24 @@ class EduVPN(object): forwardError(dropped_err) return dropped + def new_proxyguard( + self, + listen_port: int, + tcp_source_port: int, + peer: str, + setup: ProxySetup, + ) -> Proxyguard: + proxy, proxy_err = self.go_cookie_function( + self.lib.NewProxyguard, + listen_port, + tcp_source_port, + peer, + setup, + ) + if proxy_err: + forwardError(proxy_err) + return Proxyguard(self, proxy) + def cancel(self): self.jar.cancel() diff --git a/wrappers/python/eduvpn_common/types.py b/wrappers/python/eduvpn_common/types.py index 999fbdd..5e23d61 100644 --- a/wrappers/python/eduvpn_common/types.py +++ b/wrappers/python/eduvpn_common/types.py @@ -45,6 +45,7 @@ class BoolError(Structure): # The type for a Go state change callback VPNStateChange = CFUNCTYPE(c_int, c_int, c_int, c_char_p) +ProxySetup = CFUNCTYPE(c_void_p, c_int) ReadRxBytes = CFUNCTYPE(c_ulonglong) RefreshList = CFUNCTYPE(c_void_p) TokenGetter = CFUNCTYPE(c_void_p, c_char_p, c_int, POINTER(c_char), c_size_t) |
