8 Commits

Author SHA1 Message Date
2427e58cf6 add request timeouts 2024-08-07 23:23:06 +02:00
7b47ca1c58 solve small linter nitpicks 2024-08-07 22:56:19 +02:00
e4d0d9bf61 bump deps 2024-08-07 22:51:32 +02:00
6c7564062f make things a bit less ugly 2023-02-07 19:17:30 +01:00
c6ffa0e628 make linter happy 2022-12-29 23:04:13 +01:00
73414736f9 add lint workflow 2022-12-29 19:52:31 +01:00
5c8a39ad75 add user and group to systemd service example 2022-08-23 03:31:56 +02:00
deab05167d docker: trim binary, add standalone traceroute, run as own user 2022-08-23 03:20:01 +02:00
8 changed files with 105 additions and 72 deletions

7
.github/workflows/lint.yml vendored Normal file
View File

@ -0,0 +1,7 @@
name: ci
on: [push]
jobs:
test:
uses: byReqz/workflows/.github/workflows/golint_with_codeql.yml@main

View File

@ -58,8 +58,9 @@ The app currently has 4 runtime flags:
- `-o / --logfilepath` -- sets the log output file
- `-x / --disable-x-forwarded-for` -- disables checking for the X-Forwarded-For header
- `-l / --allow-private` -- allows lookups of private IP ranges
- `--request-ttl` -- sets the maximum request time to live in seconds
All the Flags also have an accompanying environment value: `PROBEHOST_LOGPATH`, `PROBEHOST_ALLOW_PRIVATE`, `PROBEHOST_LISTEN_PORT` and `PROBEHOST_DISABLE_X_FORWARDED_FOR` but the options given via commandline have priority.
All the Flags also have an accompanying environment value: `PROBEHOST_LOGPATH`, `PROBEHOST_ALLOW_PRIVATE`, `PROBEHOST_LISTEN_PORT`, `PROBEHOST_DISABLE_X_FORWARDED_FOR` and `PROBEHOST_REQUEST_TTL` but the options given via commandline have priority.
The app will log every request including the IP that's querying and show failed requests on stdout.
@ -77,6 +78,7 @@ https://[address]/[command]/[host](_[port]),[host].../[options]
- ping
- mtr
- traceroute
- nping
- [host] = can be one or more hosts query, seperated by a comma
- [port] = port to be queried, optional
- [options] = options to run the command with, seperated by a comma

View File

@ -2,11 +2,14 @@ FROM golang:latest as builder
WORKDIR /build
COPY . .
RUN go get -u
RUN CGO_ENABLED=0 go build -o probehost2
RUN CGO_ENABLED=0 go build -ldflags="-s -w" -o probehost2
FROM alpine:latest
RUN apk update
RUN apk add mtr iputils nmap-nping
RUN apk add mtr iputils nmap-nping traceroute
RUN adduser -D probehost2
COPY --from=builder /build/probehost2 /
RUN touch /probehost2.log
RUN chown probehost2:users /probehost2.log
USER probehost2
CMD ["/probehost2"]

View File

@ -10,6 +10,7 @@ services:
- PROBEHOST_ALLOW_PRIVATE=false
- PROBEHOST_DISABLE_X_FORWARDED_FOR=false
- PROBEHOST_LISTEN_PORT=8000
- PROBEHOST_REQUEST_TTL=180
ports:
- 1234:8000
volumes:

8
go.mod
View File

@ -2,9 +2,9 @@ module github.com/byReqz/probehost2
go 1.17
require github.com/sirupsen/logrus v1.8.1
require (
github.com/spf13/pflag v1.0.5 // indirect
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e // indirect
github.com/sirupsen/logrus v1.9.3
github.com/spf13/pflag v1.0.5
)
require golang.org/x/sys v0.23.0 // indirect

20
go.sum
View File

@ -1,14 +1,18 @@
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
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/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE=
github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037 h1:YyJpGZS1sBuBCzLAR1VEpK193GlqGZbnPFnPV/5Rsb4=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e h1:fLOSk5Q00efkSvAm+4xcoXD+RRmLmmulPn5I3Y9F2EM=
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.23.0 h1:YfKFowiIMvtgl1UERQoTPPToxltDeZfbj4H7dVUCwmM=
golang.org/x/sys v0.23.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

128
main.go
View File

@ -8,83 +8,86 @@ import (
"os/exec"
"strconv"
"strings"
"time"
log "github.com/sirupsen/logrus"
flag "github.com/spf13/pflag"
)
var logstdout = log.New()
var logfile = log.New()
var logStdout = log.New()
var logFile = log.New()
var listenport int
var disablexforwardedfor bool
var allowprivate bool
var listenPort = 8080 // port to listen on
var disableXForwardedFor bool // whether to disable parsing the X-Forwarded-For header or not
var allowPrivate bool // whether to allow private IP ranges or not
var requestTTL = 180 // maximum request time to live in seconds
func init() {
logstdout.SetFormatter(&log.TextFormatter{
logStdout.SetFormatter(&log.TextFormatter{
FullTimestamp: true})
logstdout.SetOutput(os.Stdout)
logstdout.SetLevel(log.InfoLevel)
var logfilepath string
logStdout.SetOutput(os.Stdout)
logStdout.SetLevel(log.InfoLevel)
if _, exists := os.LookupEnv("PROBEHOST_LOGPATH"); exists == true {
logfilepath, _ = os.LookupEnv("PROBEHOST_LOGPATH")
} else {
logfilepath = "probehost2.log"
logFilePath := "probehost2.log"
if val, exists := os.LookupEnv("PROBEHOST_LOGPATH"); exists {
logFilePath = val
}
if exists, _ := os.LookupEnv("PROBEHOST_ALLOW_PRIVATE"); exists == "true" {
allowprivate = true
} else {
allowprivate = false
}
if envvalue, exists := os.LookupEnv("PROBEHOST_LISTEN_PORT"); exists == true {
_, allowPrivate = os.LookupEnv("PROBEHOST_ALLOW_PRIVATE")
_, disableXForwardedFor = os.LookupEnv("PROBEHOST_DISABLE_X_FORWARDED_FOR")
if val, exists := os.LookupEnv("PROBEHOST_LISTEN_PORT"); exists {
var err error
listenport, err = strconv.Atoi(envvalue)
listenPort, err = strconv.Atoi(val)
if err != nil {
logstdout.Fatal("Failed to read PROBEHOST_LISTEN_PORT: ", err.Error())
logStdout.Fatal("Failed to read PROBEHOST_LISTEN_PORT: ", err.Error())
}
} else {
listenport = 8000
}
if exists, _ := os.LookupEnv("PROBEHOST_DISABLE_X_FORWARDED_FOR"); exists == "true" {
disablexforwardedfor = true
} else {
disablexforwardedfor = false
if val, exists := os.LookupEnv("PROBEHOST_REQUEST_TTL"); exists {
var err error
requestTTL, err = strconv.Atoi(val)
if err != nil {
logStdout.Fatal("Failed to read PROBEHOST_REQUEST_TTL: ", err.Error())
}
}
flag.StringVarP(&logfilepath, "logfilepath", "o", logfilepath, "sets the output file for the log")
flag.IntVarP(&listenport, "port", "p", listenport, "sets the port to listen on")
flag.BoolVarP(&disablexforwardedfor, "disable-x-forwarded-for", "x", disablexforwardedfor, "whether to show x-forwarded-for or the requesting IP")
flag.BoolVarP(&allowprivate, "allow-private", "l", allowprivate, "whether to show lookups of private IP ranges")
flag.StringVarP(&logFilePath, "logFilePath", "o", logFilePath, "sets the output file for the log")
flag.IntVarP(&listenPort, "port", "p", listenPort, "sets the port to listen on")
flag.BoolVarP(&disableXForwardedFor, "disable-x-forwarded-for", "x", disableXForwardedFor, "whether to show x-forwarded-for or the requesting IP")
flag.BoolVarP(&allowPrivate, "allow-private", "l", allowPrivate, "whether to show lookups of private IP ranges")
flag.IntVar(&requestTTL, "request-ttl", requestTTL, "sets the maximum request time to live in seconds")
flag.Parse()
logpath, err := os.OpenFile(logfilepath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0660)
logpath, err := os.OpenFile(logFilePath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0660)
if err != nil {
logstdout.Fatal("Failed to initialize the logfile: ", err.Error())
logStdout.Fatal("Failed to initialize the logFile: ", err.Error())
}
logfile.SetLevel(log.InfoLevel)
logfile.SetOutput(logpath)
logfile.Info("probehost2 initialized")
logFile.SetLevel(log.InfoLevel)
logFile.SetOutput(logpath)
logFile.Info("probehost2 initialized")
}
// runner runs the given command with the given args and returns stdout as string. Also logs all executed commands and their exit state.
func runner(remoteip string, command string, args ...string) string {
logfile.WithFields(log.Fields{
logFile.WithFields(log.Fields{
"remote_ip": remoteip,
"command": fmt.Sprint(command, args),
}).Info("request initiated:")
cmd, err := exec.Command(command, args...).Output()
if err != nil {
logstdout.WithFields(log.Fields{
logStdout.WithFields(log.Fields{
"remote_ip": remoteip,
"command": fmt.Sprint(command, args),
"error": err.Error(),
}).Warn("request failed:")
logfile.WithFields(log.Fields{
logFile.WithFields(log.Fields{
"remote_ip": remoteip,
"command": fmt.Sprint(command, args),
"error": err.Error(),
}).Warn("request failed:")
} else {
logfile.WithFields(log.Fields{
logFile.WithFields(log.Fields{
"remote_ip": remoteip,
"command": fmt.Sprint(command, args),
}).Info("request succeeded:")
@ -92,20 +95,21 @@ func runner(remoteip string, command string, args ...string) string {
return string(cmd)
}
// validatehosts checks the given host+port combinations for validity and returns valid hosts + valid ports separately.
func validatehosts(hosts []string) ([]string, []string) {
var validhosts []string
var validports []string
var validHosts []string
var validPorts []string
for _, host := range hosts {
split := strings.Split(host, "_")
host = split[0]
if hostparse := net.ParseIP(host); hostparse != nil {
if (net.IP.IsPrivate(hostparse) || net.IP.IsLoopback(hostparse)) && allowprivate {
validhosts = append(validhosts, host)
if (net.IP.IsPrivate(hostparse) || net.IP.IsLoopback(hostparse)) && allowPrivate {
validHosts = append(validHosts, host)
} else if !(net.IP.IsPrivate(hostparse) || net.IP.IsLoopback(hostparse)) {
validhosts = append(validhosts, host)
validHosts = append(validHosts, host)
}
} else if _, err := net.LookupIP(host); err == nil {
validhosts = append(validhosts, host)
validHosts = append(validHosts, host)
} else {
continue
}
@ -115,17 +119,18 @@ func validatehosts(hosts []string) ([]string, []string) {
port = split[1]
_, err := strconv.Atoi(port) // validate if port is just an int
if err == nil {
validports = append(validports, port)
validPorts = append(validPorts, port)
} else {
validports = append(validports, "0")
validPorts = append(validPorts, "0")
}
} else {
validports = append(validports, "0")
validPorts = append(validPorts, "0")
}
}
return validhosts, validports
return validHosts, validPorts
}
// parseopts matches the given user options to the valid optionmap.
func parseopts(options []string, cmdopts map[string]string) []string {
var opts []string
for _, opt := range options {
@ -134,6 +139,7 @@ func parseopts(options []string, cmdopts map[string]string) []string {
return opts
}
// prerunner processes the incoming request to send it to runner.
func prerunner(req *http.Request, cmd string, cmdopts map[string]string, defaultopts []string) string {
geturl := strings.Split(req.URL.String(), "/")
targets := strings.Split(geturl[2], ",")
@ -146,11 +152,9 @@ func prerunner(req *http.Request, cmd string, cmdopts map[string]string, default
}
var res string
var args []string
var remoteaddr string
if req.Header.Get("X-Forwarded-For") != "" && disablexforwardedfor != true {
remoteaddr := req.RemoteAddr
if req.Header.Get("X-Forwarded-For") != "" && !disableXForwardedFor {
remoteaddr = req.Header.Get("X-Forwarded-For")
} else {
remoteaddr = req.RemoteAddr
}
for i, host := range hosts {
runargs := append(args, opts...)
@ -163,6 +167,7 @@ func prerunner(req *http.Request, cmd string, cmdopts map[string]string, default
return res
}
// ping is the response handler for the ping command. It defines the allowed options.
func ping(w http.ResponseWriter, req *http.Request) {
cmd := "ping"
cmdopts := map[string]string{
@ -179,6 +184,7 @@ func ping(w http.ResponseWriter, req *http.Request) {
}
}
// mtr is the response handler for the mtr command. It defines the allowed options.
func mtr(w http.ResponseWriter, req *http.Request) {
cmd := "mtr"
cmdopts := map[string]string{
@ -195,6 +201,7 @@ func mtr(w http.ResponseWriter, req *http.Request) {
}
}
// traceroute is the response handler for the traceroute command. It defines the allowed options.
func traceroute(w http.ResponseWriter, req *http.Request) {
cmd := "traceroute"
cmdopts := map[string]string{
@ -211,6 +218,7 @@ func traceroute(w http.ResponseWriter, req *http.Request) {
}
}
// nping is the response handler for the nping command. It defines the allowed options.
func nping(w http.ResponseWriter, req *http.Request) {
cmd := "nping"
cmdopts := map[string]string{
@ -233,7 +241,13 @@ func main() {
http.HandleFunc("/tracert/", traceroute)
http.HandleFunc("/traceroute/", traceroute)
http.HandleFunc("/nping/", nping)
logstdout.Info("Serving on :", listenport)
logfile.Info("Serving on :", listenport)
_ = http.ListenAndServe(fmt.Sprint(":", listenport), nil)
server := &http.Server{
Addr: fmt.Sprint(":", listenPort),
ReadHeaderTimeout: time.Duration(requestTTL) * time.Second,
}
logStdout.Info("Serving on :", listenPort)
logFile.Info("Serving on :", listenPort)
_ = server.ListenAndServe()
}

View File

@ -8,6 +8,8 @@ StartLimitBurst=5
StartLimitIntervalSec=20
[Service]
User=1000
Group=1000
Restart=always
RestartSec=1
ExecStart=/bin/probehost2 --logfilepath "/var/log/probehost2.log" --port 8000