Compare commits

..

No commits in common. "2866c9915d8cbd2a912de766318192035b18354c" and "a94503ba7a6c201f12f22748b2fa025369e885d1" have entirely different histories.

3 changed files with 177 additions and 245 deletions

View File

@ -1,5 +1,5 @@
# probehost2 # probehost2
a http endpoint to query network diagnosis tools from remote hosts an http endpoint to query network diagnosis tools from remote hosts
- <a href="#probehost2">Overview</a> - <a href="#probehost2">Overview</a>
- <a href="#disclaimer">Disclaimer</a> - <a href="#disclaimer">Disclaimer</a>
@ -15,15 +15,14 @@ a http endpoint to query network diagnosis tools from remote hosts
- <a href="#ping">Ping</a> - <a href="#ping">Ping</a>
- <a href="#mtr">MTR</a> - <a href="#mtr">MTR</a>
- <a href="#traceroute">Traceroute</a> - <a href="#traceroute">Traceroute</a>
- <a href="#nping">Nping</a>
# Disclaimer # Disclaimer
Don't expect good or even mediocre code here. This is my first take at go and is mostly for myself to learn. Suggestions and improvements are welcome. Dont expect good or even mediocre code here. This is my first take at go and is mostly for myself to learn. Suggestions and improvements are welcome.
Please note that this project does not include any kind of rate limiting or other protection. It is therefore heavily advised to only make it publicly reachable if a reverse proxy is in place. A sample config for <a href="caddyserver.com/">Caddy</a> can be found in the `caddy` subfolder. Please note that this project does not include any kind of rate limiting or other protection. It is therefore heavily advised to only make it publicly reachable if a reverse proxy is in place. A sample config for <a href="caddyserver.com/">Caddy</a> can be found in the `caddy` subfolder.
# Installation # Installation
The runtime dependencies are currently `iputils`, `traceroute`, `nping` (usually provided by nmap) and `mtr` (sometimes called `mtr-tiny`). `iputils` and `traceroute` can be substituted by `busybox`. The runtime dependencies are currently `iputils`, `traceroute` and `mtr` (sometimes called `mtr-tiny`). `iputils` and `traceroute` can be substituted by `busybox`.
## Building ## Building
The app can be built with the latest Go toolchain. The app can be built with the latest Go toolchain.
@ -49,7 +48,7 @@ docker build -f docker/Dockerfile . -t byreqz/probehost2:latest
A compose file can also be found in `docker/docker-compose.yml`. A compose file can also be found in `docker/docker-compose.yml`.
## Proxy ## Proxy
It's recommended to only run this app together with a rate-limiting reverse-proxy. An example configuration for <a href="caddyserver.com/">Caddy</a> can be found in the `caddy` subfolder. Its recommended to only run this app together with a rate-limiting reverse-proxy. An example configuration for <a href="caddyserver.com/">Caddy</a> can be found in the `caddy` subfolder.
# Usage # Usage
## Server ## Server
@ -59,9 +58,9 @@ 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 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 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 that's 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.
@ -69,7 +68,7 @@ Requests that contain an X-Forwarded-For header (implying the app is behind a re
### General ### General
The app can be queried via HTTP/HTTPS with the following scheme: The app can be queried via HTTP/HTTPS with the following scheme:
``` ```
https://[address]/[command]/[host](_[port]),[host].../[options] https://[address]/[command]/[hosts]/[options]
``` ```
- [address] = the IP or domain serving the site - [address] = the IP or domain serving the site
@ -77,8 +76,7 @@ https://[address]/[command]/[host](_[port]),[host].../[options]
- ping - ping
- mtr - mtr
- traceroute - traceroute
- [host] = can be one or more hosts query, seperated by a comma - [hosts] = 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 - [options] = options to run the command with, seperated by a comma
All inputs are validated and invalid input is discarded. If the request contains no valid data, the server will return HTTP 500. All inputs are validated and invalid input is discarded. If the request contains no valid data, the server will return HTTP 500.
@ -133,7 +131,7 @@ Available options are:
- `c10` / `count10`: send 10 pings - `c10` / `count10`: send 10 pings
Example query: Example query:
```sh ```
$ curl http://localhost:8000/mtr/localhost/c1,z $ curl http://localhost:8000/mtr/localhost/c1,z
Start: 2022-01-02T00:06:56+0100 Start: 2022-01-02T00:06:56+0100
HOST: xxx Loss% Snt Last Avg Best Wrst StDev HOST: xxx Loss% Snt Last Avg Best Wrst StDev
@ -157,37 +155,9 @@ Available options are:
- `b` / `back`: Guess the number of hops in the backward path and print if it differs - `b` / `back`: Guess the number of hops in the backward path and print if it differs
Example query: Example query:
```sh ```
$ curl http://localhost:8000/tracert/localhost/i $ curl http://localhost:8000/tracert/localhost/i
traceroute to localhost (127.0.0.1), 30 hops max, 60 byte packets traceroute to localhost (127.0.0.1), 30 hops max, 60 byte packets
1 localhost (127.0.0.1) 0.063 ms 0.008 ms 0.006 ms 1 localhost (127.0.0.1) 0.063 ms 0.008 ms 0.006 ms
```
### Nping
The default options are:
- `c3`: send 3 pings
Available options are:
- `4` / `force4`: force IPv4
- `6` / `force6`: force IPv6
- `u` / `udp`: use UDP
- `t` / `tcp`: use TCP
- `v` / `verbose`: be verbose
- `c1` / `count1`: send 1 ping
- `c3` / `count3`: send 3 pings
- `c5` / `count5`: send 5 pings
Example query:
```sh
$ curl localhost:8000/nping/localhost_22
Starting Nping 0.7.92 ( https://nmap.org/nping ) at 2022-05-29 15:28 CEST
SENT (0.0022s) Starting TCP Handshake > localhost:22 (127.0.0.1:22)
RCVD (0.0133s) Handshake with localhost:22 (127.0.0.1:22) completed
SENT (1.0041s) Starting TCP Handshake > localhost:22 (127.0.0.1:22)
RCVD (1.0089s) Handshake with localhost:22 (127.0.0.1:22) completed
SENT (2.0071s) Starting TCP Handshake > localhost:22 (127.0.0.1:22)
RCVD (2.0090s) Handshake with localhost:22 (127.0.0.1:22) completed
Max rtt: 11.130ms | Min rtt: 1.945ms | Avg rtt: 5.965ms
TCP connection attempts: 3 | Successful connections: 3 | Failed: 0 (0.00%)
Nping done: 1 IP address pinged in 2.01 seconds
``` ```

View File

@ -6,7 +6,7 @@ RUN CGO_ENABLED=0 go build -o probehost2
FROM alpine:latest FROM alpine:latest
RUN apk update RUN apk update
RUN apk add mtr iputils nmap-nping RUN apk add mtr iputils
COPY --from=builder /build/probehost2 / COPY --from=builder /build/probehost2 /
RUN touch /probehost2.log RUN touch /probehost2.log
CMD ["/probehost2"] CMD ["/probehost2"]

368
main.go
View File

@ -1,16 +1,16 @@
package main package main
import ( import (
"fmt" "fmt"
"net" "os"
"net/http" "os/exec"
"os" "strings"
"os/exec" "net/http"
"strconv" "net"
"strings" "strconv"
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()
@ -21,219 +21,181 @@ var disablexforwardedfor bool
var allowprivate bool var allowprivate bool
func init() { func init() {
logstdout.SetFormatter(&log.TextFormatter{ logstdout.SetFormatter(&log.TextFormatter{
FullTimestamp: true}) FullTimestamp: true})
logstdout.SetOutput(os.Stdout) logstdout.SetOutput(os.Stdout)
logstdout.SetLevel(log.InfoLevel) logstdout.SetLevel(log.InfoLevel)
var logfilepath string var logfilepath string
if _, exists := os.LookupEnv("PROBEHOST_LOGPATH"); exists == true { if _, exists := os.LookupEnv("PROBEHOST_LOGPATH"); exists == true {
logfilepath, _ = os.LookupEnv("PROBEHOST_LOGPATH") logfilepath, _ = os.LookupEnv("PROBEHOST_LOGPATH")
} else { } else {
logfilepath = "probehost2.log" logfilepath = "probehost2.log"
} }
if exists, _ := os.LookupEnv("PROBEHOST_ALLOW_PRIVATE"); exists == "true" { if exists, _ := os.LookupEnv("PROBEHOST_ALLOW_PRIVATE"); exists == "true" {
allowprivate = true allowprivate = true
} else { } else {
allowprivate = false allowprivate = false
} }
if envvalue, exists := os.LookupEnv("PROBEHOST_LISTEN_PORT"); exists == true { if envvalue, exists := os.LookupEnv("PROBEHOST_LISTEN_PORT"); exists == true {
var err error var err error
listenport, err = strconv.Atoi(envvalue) listenport, err = strconv.Atoi(envvalue)
if err != nil { if err != nil {
logstdout.Fatal("Failed to read PROBEHOST_LISTEN_PORT: ", err.Error()) logstdout.Fatal("Failed to read PROBEHOST_LISTEN_PORT: ", err.Error())
} }
} else { } else {
listenport = 8000 listenport = 8000
} }
if exists, _ := os.LookupEnv("PROBEHOST_DISABLE_X_FORWARDED_FOR"); exists == "true" { if exists, _ := os.LookupEnv("PROBEHOST_DISABLE_X_FORWARDED_FOR"); exists == "true" {
disablexforwardedfor = true disablexforwardedfor = true
} else { } else {
disablexforwardedfor = false disablexforwardedfor = false
} }
flag.StringVarP(&logfilepath, "logfilepath", "o", logfilepath, "sets the output file for the log") 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.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(&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.BoolVarP(&allowprivate, "allow-private", "l", allowprivate, "whether to show lookups of private IP ranges")
flag.Parse() 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, []string) { func validatehosts(hosts []string) []string{
var validhosts []string var valid []string
var validports []string for _, host := range hosts {
for _, host := range hosts { if hostparse := net.ParseIP(host); hostparse != nil {
split := strings.Split(host, "_") if (net.IP.IsPrivate(hostparse) || net.IP.IsLoopback(hostparse)) && allowprivate {
host = split[0] valid = append(valid, host)
if hostparse := net.ParseIP(host); hostparse != nil { } else if ! (net.IP.IsPrivate(hostparse) || net.IP.IsLoopback(hostparse)) {
if (net.IP.IsPrivate(hostparse) || net.IP.IsLoopback(hostparse)) && allowprivate { valid = append(valid, host)
validhosts = append(validhosts, host) }
} else if !(net.IP.IsPrivate(hostparse) || net.IP.IsLoopback(hostparse)) { } else if _, err := net.LookupIP(host); err == nil {
validhosts = append(validhosts, host) valid = append(valid, host)
} }
} else if _, err := net.LookupIP(host); err == nil { }
validhosts = append(validhosts, host) return valid
} 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, ports := validatehosts(targets) hosts := 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 i, host := range hosts { for _, host := range hosts {
runargs := append(args, opts...) args = append(args, opts...)
if ports[i] != "0" && cmd == "nping" { args = append(args, host)
runargs = append(runargs, "-p"+ports[i]) res = fmt.Sprint(res, runner(remoteaddr, cmd, args...), "\n")
} }
runargs = append(runargs, host) return res
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) == "" {
http.Error(w, "500: Internal Server Error", http.StatusInternalServerError) fmt.Fprintln(w, 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) == "" {
http.Error(w, "500: Internal Server Error", http.StatusInternalServerError) fmt.Fprintln(w, 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) == "" {
http.Error(w, "500: Internal Server Error", http.StatusInternalServerError) fmt.Fprintln(w, 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)
http.HandleFunc("/nping/", nping) logstdout.Info("Serving on :", listenport)
logstdout.Info("Serving on :", listenport) logfile.Info("Serving on :", listenport)
logfile.Info("Serving on :", listenport) http.ListenAndServe(fmt.Sprint(":", listenport), nil)
_ = http.ListenAndServe(fmt.Sprint(":", listenport), nil)
} }