Skip to content

Commit 623be5e

Browse files
Merge pull request #20 from fosrl/dev
Cleanup & Updown Script
2 parents 683312c + 72d264d commit 623be5e

File tree

5 files changed

+187
-22
lines changed

5 files changed

+187
-22
lines changed

README.md

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,8 @@ When Newt receives WireGuard control messages, it will use the information encod
3636
- `secret`: A unique secret (not shared and kept private) used to authenticate the client ID with the websocket in order to receive commands.
3737
- `dns`: DNS server to use to resolve the endpoint
3838
- `log-level` (optional): The log level to use. Default: INFO
39-
39+
- `updown` (optional): A script to be called when targets are added or removed.
40+
4041
Example:
4142

4243
```bash
@@ -92,6 +93,20 @@ WantedBy=multi-user.target
9293

9394
Make sure to `mv ./newt /usr/local/bin/newt`!
9495

96+
### Updown
97+
98+
You can pass in a updown script for Newt to call when it is adding or removing a target:
99+
100+
`--updown "python3 test.py"`
101+
102+
It will get called with args when a target is added:
103+
`python3 test.py add tcp localhost:8556`
104+
`python3 test.py remove tcp localhost:8556`
105+
106+
Returning a string from the script in the format of a target (`ip:dst` so `10.0.0.1:8080`) it will override the target and use this value instead to proxy.
107+
108+
You can look at updown.py as a reference script to get started!
109+
95110
## Build
96111

97112
### Container

go.mod

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,19 @@ go 1.23.1
44

55
toolchain go1.23.2
66

7-
require golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173
7+
require (
8+
github.com/gorilla/websocket v1.5.3
9+
golang.org/x/net v0.30.0
10+
golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173
11+
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230429144221-925a1e7659e6
12+
gvisor.dev/gvisor v0.0.0-20230927004350-cbd86285d259
13+
)
814

915
require (
1016
github.com/google/btree v1.1.2 // indirect
11-
github.com/gorilla/websocket v1.5.3 // indirect
17+
github.com/google/go-cmp v0.6.0 // indirect
1218
golang.org/x/crypto v0.28.0 // indirect
13-
golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8 // indirect
14-
golang.org/x/net v0.30.0 // indirect
1519
golang.org/x/sys v0.26.0 // indirect
1620
golang.org/x/time v0.7.0 // indirect
1721
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect
18-
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230429144221-925a1e7659e6 // indirect
19-
gvisor.dev/gvisor v0.0.0-20230927004350-cbd86285d259 // indirect
2022
)

go.sum

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
github.com/google/btree v1.1.2 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU=
22
github.com/google/btree v1.1.2/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4=
3+
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
4+
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
35
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
46
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
57
golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw=
68
golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U=
7-
golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8 h1:yqrTHse8TCMW1M1ZCP+VAR/l0kKxwaAIqN/il7x4voA=
8-
golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8/go.mod h1:tujkw807nyEEAamNbDrEGzRav+ilXA7PCRAd6xsmwiU=
99
golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4=
1010
golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU=
1111
golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo=

main.go

Lines changed: 84 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111
"net"
1212
"net/netip"
1313
"os"
14+
"os/exec"
1415
"os/signal"
1516
"strconv"
1617
"strings"
@@ -244,26 +245,28 @@ func resolveDomain(domain string) (string, error) {
244245
return ipAddr, nil
245246
}
246247

247-
func main() {
248-
var (
249-
endpoint string
250-
id string
251-
secret string
252-
mtu string
253-
mtuInt int
254-
dns string
255-
privateKey wgtypes.Key
256-
err error
257-
logLevel string
258-
)
248+
var (
249+
endpoint string
250+
id string
251+
secret string
252+
mtu string
253+
mtuInt int
254+
dns string
255+
privateKey wgtypes.Key
256+
err error
257+
logLevel string
258+
updownScript string
259+
)
259260

261+
func main() {
260262
// if PANGOLIN_ENDPOINT, NEWT_ID, and NEWT_SECRET are set as environment variables, they will be used as default values
261263
endpoint = os.Getenv("PANGOLIN_ENDPOINT")
262264
id = os.Getenv("NEWT_ID")
263265
secret = os.Getenv("NEWT_SECRET")
264266
mtu = os.Getenv("MTU")
265267
dns = os.Getenv("DNS")
266268
logLevel = os.Getenv("LOG_LEVEL")
269+
updownScript = os.Getenv("UPDOWN_SCRIPT")
267270

268271
if endpoint == "" {
269272
flag.StringVar(&endpoint, "endpoint", "", "Endpoint of your pangolin server")
@@ -283,6 +286,9 @@ func main() {
283286
if logLevel == "" {
284287
flag.StringVar(&logLevel, "log-level", "INFO", "Log level (DEBUG, INFO, WARN, ERROR, FATAL)")
285288
}
289+
if updownScript == "" {
290+
flag.StringVar(&updownScript, "updown", "", "Path to updown script to be called when targets are added or removed")
291+
}
286292

287293
// do a --version check
288294
version := flag.Bool("version", false, "Print the version")
@@ -586,6 +592,18 @@ func updateTargets(pm *proxy.ProxyManager, action string, tunnelIP string, proto
586592

587593
if action == "add" {
588594
target := parts[1] + ":" + parts[2]
595+
596+
// Call updown script if provided
597+
processedTarget := target
598+
if updownScript != "" {
599+
newTarget, err := executeUpdownScript(action, proto, target)
600+
if err != nil {
601+
logger.Warn("Updown script error: %v", err)
602+
} else if newTarget != "" {
603+
processedTarget = newTarget
604+
}
605+
}
606+
589607
// Only remove the specific target if it exists
590608
err := pm.RemoveTarget(proto, tunnelIP, port)
591609
if err != nil {
@@ -596,10 +614,21 @@ func updateTargets(pm *proxy.ProxyManager, action string, tunnelIP string, proto
596614
}
597615

598616
// Add the new target
599-
pm.AddTarget(proto, tunnelIP, port, target)
617+
pm.AddTarget(proto, tunnelIP, port, processedTarget)
600618

601619
} else if action == "remove" {
602620
logger.Info("Removing target with port %d", port)
621+
622+
target := parts[1] + ":" + parts[2]
623+
624+
// Call updown script if provided
625+
if updownScript != "" {
626+
_, err := executeUpdownScript(action, proto, target)
627+
if err != nil {
628+
logger.Warn("Updown script error: %v", err)
629+
}
630+
}
631+
603632
err := pm.RemoveTarget(proto, tunnelIP, port)
604633
if err != nil {
605634
logger.Error("Failed to remove target: %v", err)
@@ -610,3 +639,45 @@ func updateTargets(pm *proxy.ProxyManager, action string, tunnelIP string, proto
610639

611640
return nil
612641
}
642+
643+
func executeUpdownScript(action, proto, target string) (string, error) {
644+
if updownScript == "" {
645+
return target, nil
646+
}
647+
648+
// Split the updownScript in case it contains spaces (like "/usr/bin/python3 script.py")
649+
parts := strings.Fields(updownScript)
650+
if len(parts) == 0 {
651+
return target, fmt.Errorf("invalid updown script command")
652+
}
653+
654+
var cmd *exec.Cmd
655+
if len(parts) == 1 {
656+
// If it's a single executable
657+
logger.Info("Executing updown script: %s %s %s %s", updownScript, action, proto, target)
658+
cmd = exec.Command(parts[0], action, proto, target)
659+
} else {
660+
// If it includes interpreter and script
661+
args := append(parts[1:], action, proto, target)
662+
logger.Info("Executing updown script: %s %s %s %s %s", parts[0], strings.Join(parts[1:], " "), action, proto, target)
663+
cmd = exec.Command(parts[0], args...)
664+
}
665+
666+
output, err := cmd.Output()
667+
if err != nil {
668+
if exitErr, ok := err.(*exec.ExitError); ok {
669+
return "", fmt.Errorf("updown script execution failed (exit code %d): %s",
670+
exitErr.ExitCode(), string(exitErr.Stderr))
671+
}
672+
return "", fmt.Errorf("updown script execution failed: %v", err)
673+
}
674+
675+
// If the script returns a new target, use it
676+
newTarget := strings.TrimSpace(string(output))
677+
if newTarget != "" {
678+
logger.Info("Updown script returned new target: %s", newTarget)
679+
return newTarget, nil
680+
}
681+
682+
return target, nil
683+
}

updown.py

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
"""
2+
Sample updown script for Newt proxy
3+
Usage: update.py <action> <protocol> <target>
4+
5+
Parameters:
6+
- action: 'add' or 'remove'
7+
- protocol: 'tcp' or 'udp'
8+
- target: the target address in format 'host:port'
9+
10+
If the action is 'add', the script can return a modified target that
11+
will be used instead of the original.
12+
"""
13+
14+
import sys
15+
import logging
16+
import json
17+
from datetime import datetime
18+
19+
# Configure logging
20+
LOG_FILE = "/tmp/newt-updown.log"
21+
logging.basicConfig(
22+
filename=LOG_FILE,
23+
level=logging.INFO,
24+
format='%(asctime)s - %(levelname)s - %(message)s'
25+
)
26+
27+
def log_event(action, protocol, target):
28+
"""Log each event to a file for auditing purposes"""
29+
timestamp = datetime.now().isoformat()
30+
event = {
31+
"timestamp": timestamp,
32+
"action": action,
33+
"protocol": protocol,
34+
"target": target
35+
}
36+
logging.info(json.dumps(event))
37+
38+
def handle_add(protocol, target):
39+
"""Handle 'add' action"""
40+
logging.info(f"Adding {protocol} target: {target}")
41+
42+
def handle_remove(protocol, target):
43+
"""Handle 'remove' action"""
44+
logging.info(f"Removing {protocol} target: {target}")
45+
# For remove action, no return value is expected or used
46+
47+
def main():
48+
# Check arguments
49+
if len(sys.argv) != 4:
50+
logging.error(f"Invalid arguments: {sys.argv}")
51+
sys.exit(1)
52+
53+
action = sys.argv[1]
54+
protocol = sys.argv[2]
55+
target = sys.argv[3]
56+
57+
# Log the event
58+
log_event(action, protocol, target)
59+
60+
# Handle the action
61+
if action == "add":
62+
new_target = handle_add(protocol, target)
63+
# Print the new target to stdout (if empty, no change will be made)
64+
if new_target and new_target != target:
65+
print(new_target)
66+
elif action == "remove":
67+
handle_remove(protocol, target)
68+
else:
69+
logging.error(f"Unknown action: {action}")
70+
sys.exit(1)
71+
72+
if __name__ == "__main__":
73+
try:
74+
main()
75+
except Exception as e:
76+
logging.error(f"Unhandled exception: {e}")
77+
sys.exit(1)

0 commit comments

Comments
 (0)