7 Commits

Author SHA1 Message Date
3b4e96509b explicitly dont handle returns in certain places 2022-05-29 15:09:58 +02:00
a0e42d369a fix command order if host is invalid but port proper 2022-05-29 15:07:01 +02:00
871fe76df5 return proper http errors 2022-05-29 15:05:13 +02:00
2b8510c5ee cleanup nping commands 2022-05-29 14:26:09 +02:00
955f1253af add nping handler, port support and fmt code 2022-05-29 14:20:06 +02:00
a94503ba7a add caddy config example 2022-01-07 10:05:29 +01:00
ce95696ef0 add environment values 2022-01-06 23:37:03 +01:00
7 changed files with 254 additions and 141 deletions

View File

@ -58,6 +58,8 @@ The app currently has 4 runtime flags:
- `-x / --disable-x-forwarded-for` -- disables checking for the X-Forwarded-For header - `-x / --disable-x-forwarded-for` -- disables checking for the X-Forwarded-For header
- `-l / --allow-private` -- allows lookups of private IP ranges - `-l / --allow-private` -- allows lookups of private IP ranges
All of 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.
The app will log every request including the IP thats querying and show failed requests on stdout. The app will log every request including the IP thats querying and show failed requests on stdout.
Requests that contain an X-Forwarded-For header (implying the app is behind a reverse proxy) will automatically log that address instead of the requesting IP (the proxy itself), this can be turned off with -x. Requests that contain an X-Forwarded-For header (implying the app is behind a reverse proxy) will automatically log that address instead of the requesting IP (the proxy itself), this can be turned off with -x.

29
caddy/Caddyfile Normal file
View File

@ -0,0 +1,29 @@
{
email [your email]
order rate_limit before basicauth
}
:80, :443 {
redir * https://[your domain]
}
[your domain] {
reverse_proxy [host]:[port]
log {
output file [path] {
roll_size 10GiB
roll_keep 10
}
level INFO
}
handle_errors {
respond "{http.error.status_code} {http.error.status_text}"
}
rate_limit {
zone dynamic {
key {remote_host}
events 2
window 5s
}
}
}

8
caddy/Dockerfile Normal file
View File

@ -0,0 +1,8 @@
FROM caddy:builder AS builder
RUN xcaddy build \
--with github.com/mholt/caddy-ratelimit \
--with github.com/caddy-dns/cloudflare
FROM caddy:latest
COPY --from=builder /usr/bin/caddy /usr/bin/caddy

5
caddy/README.md Normal file
View File

@ -0,0 +1,5 @@
# caddy
This folder contains a sample configuration for caddy with ratelimiting enabled. this will allow 2 requests per IP every 5 seconds. For more on that, check here: https://github.com/mholt/caddy-ratelimit
I have also included a dockerfile to build caddy with the ratelimiting module.

1
caddy/build.sh Executable file
View File

@ -0,0 +1 @@
xcaddy build --with github.com/mholt/caddy-ratelimit

View File

@ -5,6 +5,11 @@ services:
container_name: probehost2 container_name: probehost2
image: byreqz/probehost2:latest image: byreqz/probehost2:latest
restart: unless-stopped restart: unless-stopped
environment:
- PROBEHOST_LOGPATH=/probehost2.log
- PROBEHOST_ALLOW_PRIVATE=false
- PROBEHOST_DISABLE_X_FORWARDED_FOR=false
- PROBEHOST_LISTEN_PORT=8000
ports: ports:
- 1234:8000 - 1234:8000
volumes: volumes:

345
main.go
View File

@ -1,15 +1,16 @@
package main package main
import ( import (
"fmt" "fmt"
"os" "net"
"os/exec" "net/http"
"strings" "os"
"net/http" "os/exec"
"net" "strconv"
"strings"
log "github.com/sirupsen/logrus"
flag "github.com/spf13/pflag"
log "github.com/sirupsen/logrus"
flag "github.com/spf13/pflag"
) )
var logstdout = log.New() var logstdout = log.New()
@ -20,157 +21,219 @@ var disablexforwardedfor bool
var allowprivate bool var allowprivate bool
func init() { func init() {
var logfilepath string logstdout.SetFormatter(&log.TextFormatter{
flag.StringVarP(&logfilepath, "logfilepath", "o","probehost2.log", "sets the output file for the log") FullTimestamp: true})
flag.IntVarP(&listenport, "port", "p", 8000, "sets the port to listen on") logstdout.SetOutput(os.Stdout)
flag.BoolVarP(&disablexforwardedfor, "disable-x-forwarded-for", "x", false, "whether to show x-forwarded-for or the requesting IP") logstdout.SetLevel(log.InfoLevel)
flag.BoolVarP(&allowprivate, "allow-private", "l", false, "whether to show lookups of private IP ranges") var logfilepath string
flag.Parse()
logstdout.SetFormatter(&log.TextFormatter{ if _, exists := os.LookupEnv("PROBEHOST_LOGPATH"); exists == true {
FullTimestamp: true}) logfilepath, _ = os.LookupEnv("PROBEHOST_LOGPATH")
logstdout.SetOutput(os.Stdout) } else {
logstdout.SetLevel(log.InfoLevel) logfilepath = "probehost2.log"
}
if exists, _ := os.LookupEnv("PROBEHOST_ALLOW_PRIVATE"); exists == "true" {
allowprivate = true
} else {
allowprivate = false
}
if envvalue, exists := os.LookupEnv("PROBEHOST_LISTEN_PORT"); exists == true {
var err error
listenport, err = strconv.Atoi(envvalue)
if err != nil {
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
}
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.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 { 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.SetLevel(log.InfoLevel)
logfile.SetOutput(logpath) logfile.SetOutput(logpath)
logfile.Info("probehost2 initialized") logfile.Info("probehost2 initialized")
} }
func runner(remoteip string, command string, args... string) string{ func runner(remoteip string, command string, args ...string) string {
logfile.WithFields(log.Fields{ logfile.WithFields(log.Fields{
"remote_ip": remoteip, "remote_ip": remoteip,
"command": fmt.Sprint(command, args), "command": fmt.Sprint(command, args),
}).Info("request initiated:") }).Info("request initiated:")
cmd, err := exec.Command(command, args...).Output() cmd, err := exec.Command(command, args...).Output()
if err != nil { if err != nil {
logstdout.WithFields(log.Fields{ logstdout.WithFields(log.Fields{
"remote_ip": remoteip, "remote_ip": remoteip,
"command": fmt.Sprint(command, args), "command": fmt.Sprint(command, args),
"error": err.Error(), "error": err.Error(),
}).Warn("request failed:") }).Warn("request failed:")
logfile.WithFields(log.Fields{ logfile.WithFields(log.Fields{
"remote_ip": remoteip, "remote_ip": remoteip,
"command": fmt.Sprint(command, args), "command": fmt.Sprint(command, args),
"error": err.Error(), "error": err.Error(),
}).Warn("request failed:") }).Warn("request failed:")
} else { } else {
logfile.WithFields(log.Fields{ logfile.WithFields(log.Fields{
"remote_ip": remoteip, "remote_ip": remoteip,
"command": fmt.Sprint(command, args), "command": fmt.Sprint(command, args),
}).Info("request succeeded:") }).Info("request succeeded:")
} }
return string(cmd) return string(cmd)
} }
func validatehosts(hosts []string) []string{ func validatehosts(hosts []string) ([]string, []string) {
var valid []string var validhosts []string
for _, host := range hosts { var validports []string
if hostparse := net.ParseIP(host); hostparse != nil { for _, host := range hosts {
if (net.IP.IsPrivate(hostparse) || net.IP.IsLoopback(hostparse)) && allowprivate { split := strings.Split(host, "_")
valid = append(valid, host) host = split[0]
} else if ! (net.IP.IsPrivate(hostparse) || net.IP.IsLoopback(hostparse)) { if hostparse := net.ParseIP(host); hostparse != nil {
valid = append(valid, host) if (net.IP.IsPrivate(hostparse) || net.IP.IsLoopback(hostparse)) && allowprivate {
} validhosts = append(validhosts, host)
} else if _, err := net.LookupIP(host); err == nil { } else if !(net.IP.IsPrivate(hostparse) || net.IP.IsLoopback(hostparse)) {
valid = append(valid, host) validhosts = append(validhosts, host)
} }
} } else if _, err := net.LookupIP(host); err == nil {
return valid validhosts = append(validhosts, host)
} else {
continue
}
var port string
if len(split) > 1 {
port = split[1]
_, err := strconv.Atoi(port) // validate if port is just an int
if err == nil {
validports = append(validports, port)
} else {
validports = append(validports, "0")
}
} else {
validports = append(validports, "0")
}
}
return validhosts, validports
} }
func parseopts(options []string, cmdopts map[string]string) []string{ func parseopts(options []string, cmdopts map[string]string) []string {
var opts []string var opts []string
for _, opt := range options { for _, opt := range options {
opts = append(opts, cmdopts[opt]) opts = append(opts, cmdopts[opt])
} }
return opts return opts
} }
func prerunner(req *http.Request, cmd string, cmdopts map[string]string, defaultopts []string) string{ func prerunner(req *http.Request, cmd string, cmdopts map[string]string, defaultopts []string) string {
geturl := strings.Split(req.URL.String(), "/") geturl := strings.Split(req.URL.String(), "/")
targets := strings.Split(geturl[2], ",") targets := strings.Split(geturl[2], ",")
hosts := validatehosts(targets) hosts, ports := validatehosts(targets)
var opts []string var opts []string
opts = append(opts, defaultopts...) opts = append(opts, defaultopts...)
if len(geturl) > 3 && len(geturl[3]) > 0 { if len(geturl) > 3 && len(geturl[3]) > 0 {
options := strings.Split(geturl[3], ",") options := strings.Split(geturl[3], ",")
opts = append(opts, parseopts(options, cmdopts)...) opts = append(opts, parseopts(options, cmdopts)...)
} }
var res string var res string
var args []string var args []string
var remoteaddr string var remoteaddr string
if req.Header.Get("X-Forwarded-For") != "" && disablexforwardedfor != true { if req.Header.Get("X-Forwarded-For") != "" && disablexforwardedfor != true {
remoteaddr = req.Header.Get("X-Forwarded-For") remoteaddr = req.Header.Get("X-Forwarded-For")
} else { } else {
remoteaddr = req.RemoteAddr remoteaddr = req.RemoteAddr
} }
for _, host := range hosts { for i, host := range hosts {
args = append(args, opts...) runargs := append(args, opts...)
args = append(args, host) if ports[i] != "0" && cmd == "nping" {
res = fmt.Sprint(res, runner(remoteaddr, cmd, args...), "\n") runargs = append(runargs, "-p"+ports[i])
} }
return res runargs = append(runargs, host)
res = fmt.Sprint(res, runner(remoteaddr, cmd, runargs...), "\n")
}
return res
} }
func ping(w http.ResponseWriter, req *http.Request) { func ping(w http.ResponseWriter, req *http.Request) {
cmd := "ping" cmd := "ping"
cmdopts := map[string]string{ cmdopts := map[string]string{
"4": "-4", "6": "-6", "d": "-D", "n": "-n", "v": "-v", "c1": "-c1", "c5": "-c5", "c10": "-c10", "4": "-4", "6": "-6", "d": "-D", "n": "-n", "v": "-v", "c1": "-c1", "c5": "-c5", "c10": "-c10",
"force4": "-4", "force6": "-6", "timestamps": "-D", "nodns": "-n", "verbose": "-v", "count1": "-c1", "count5": "-c5", "count10": "-c10", "force4": "-4", "force6": "-6", "timestamps": "-D", "nodns": "-n", "verbose": "-v", "count1": "-c1", "count5": "-c5", "count10": "-c10",
} }
var defaultopts []string var defaultopts []string
defaultopts = append(defaultopts, "-c10") defaultopts = append(defaultopts, "-c10")
res := prerunner(req, cmd, cmdopts, defaultopts) res := prerunner(req, cmd, cmdopts, defaultopts)
if strings.TrimSpace(res) == "" { if strings.TrimSpace(res) == "" {
fmt.Fprintln(w, http.StatusInternalServerError) http.Error(w, "500: Internal Server Error", http.StatusInternalServerError)
} else { } else {
fmt.Fprint(w, strings.TrimSpace(res), "\n") _, _ = fmt.Fprint(w, strings.TrimSpace(res), "\n")
} }
} }
func mtr(w http.ResponseWriter, req *http.Request) { func mtr(w http.ResponseWriter, req *http.Request) {
cmd := "mtr" cmd := "mtr"
cmdopts := map[string]string{ cmdopts := map[string]string{
"4": "-4", "6": "-6", "u": "-u", "t": "-T", "e": "-e", "x": "-x", "n": "-n", "b": "-b", "z": "-z", "c1": "-c1", "c5": "-c5", "c10": "-c10", "4": "-4", "6": "-6", "u": "-u", "t": "-T", "e": "-e", "x": "-x", "n": "-n", "b": "-b", "z": "-z", "c1": "-c1", "c5": "-c5", "c10": "-c10",
"force4": "-4", "force6": "-6", "udp": "-u", "tcp": "-T", "ext": "-e", "xml": "-x", "nodns": "-n", "cmb": "-b", "asn": "-z", "count1": "-c1", "count5": "-c5", "count10": "-c10", "force4": "-4", "force6": "-6", "udp": "-u", "tcp": "-T", "ext": "-e", "xml": "-x", "nodns": "-n", "cmb": "-b", "asn": "-z", "count1": "-c1", "count5": "-c5", "count10": "-c10",
} }
var defaultopts []string var defaultopts []string
defaultopts = append(defaultopts, "-r", "-w", "-c10") defaultopts = append(defaultopts, "-r", "-w", "-c10")
res := prerunner(req, cmd, cmdopts, defaultopts) res := prerunner(req, cmd, cmdopts, defaultopts)
if strings.TrimSpace(res) == "" { if strings.TrimSpace(res) == "" {
fmt.Fprintln(w, http.StatusInternalServerError) http.Error(w, "500: Internal Server Error", http.StatusInternalServerError)
} else { } else {
fmt.Fprint(w, strings.TrimSpace(res), "\n") _, _ = fmt.Fprint(w, strings.TrimSpace(res), "\n")
} }
} }
func traceroute(w http.ResponseWriter, req *http.Request) { func traceroute(w http.ResponseWriter, req *http.Request) {
cmd := "traceroute" cmd := "traceroute"
cmdopts := map[string]string{ cmdopts := map[string]string{
"4": "-4", "6": "-6", "f": "-F", "i": "-I", "t": "-T", "n": "-n", "u": "-U", "ul": "-UL", "d": "-D", "b": "--back", "4": "-4", "6": "-6", "f": "-F", "i": "-I", "t": "-T", "n": "-n", "u": "-U", "ul": "-UL", "d": "-D", "b": "--back",
"force4": "-4", "force6": "-6", "dnf": "-F", "icmp": "-I", "tcp": "-T", "nodns": "-n", "udp": "-U", "udplite": "-UL", "dccp": "-D", "back": "--back", "force4": "-4", "force6": "-6", "dnf": "-F", "icmp": "-I", "tcp": "-T", "nodns": "-n", "udp": "-U", "udplite": "-UL", "dccp": "-D", "back": "--back",
} }
var defaultopts []string var defaultopts []string
//defaultopts = append(defaultopts) // no default options for traceroute //defaultopts = append(defaultopts) // no default options for traceroute
res := prerunner(req, cmd, cmdopts, defaultopts) res := prerunner(req, cmd, cmdopts, defaultopts)
if strings.TrimSpace(res) == "" { if strings.TrimSpace(res) == "" {
fmt.Fprintln(w, http.StatusInternalServerError) http.Error(w, "500: Internal Server Error", http.StatusInternalServerError)
} else { } else {
fmt.Fprint(w, strings.TrimSpace(res), "\n") _, _ = fmt.Fprint(w, strings.TrimSpace(res), "\n")
} }
}
func nping(w http.ResponseWriter, req *http.Request) {
cmd := "nping"
cmdopts := map[string]string{
"4": "-4", "6": "-6", "u": "--udp", "t": "--tcp-connect", "v": "-v", "c1": "-c1", "c3": "-c3", "c5": "-c5",
"force4": "-4", "force6": "-6", "udp": "--udp", "tcp": "--tcp-connect", "verbose": "-v", "count1": "-c1", "count3": "-c3", "count5": "-c5",
}
var defaultopts []string
defaultopts = append(defaultopts, "-c3")
res := prerunner(req, cmd, cmdopts, defaultopts)
if strings.TrimSpace(res) == "" {
http.Error(w, "500: Internal Server Error", http.StatusInternalServerError)
} else {
_, _ = fmt.Fprint(w, strings.TrimSpace(res), "\n")
}
} }
func main() { func main() {
http.HandleFunc("/ping/", ping) http.HandleFunc("/ping/", ping)
http.HandleFunc("/mtr/", mtr) http.HandleFunc("/mtr/", mtr)
http.HandleFunc("/tracert/", traceroute) http.HandleFunc("/tracert/", traceroute)
http.HandleFunc("/traceroute/", traceroute) http.HandleFunc("/traceroute/", traceroute)
logstdout.Info("Serving on :", listenport) http.HandleFunc("/nping/", nping)
logfile.Info("Serving on :", listenport) logstdout.Info("Serving on :", listenport)
http.ListenAndServe(fmt.Sprint(":", listenport), nil) logfile.Info("Serving on :", listenport)
} _ = http.ListenAndServe(fmt.Sprint(":", listenport), nil)
}