├── .gitignore ├── .travis.yml ├── main.go ├── README.md ├── LICENSE ├── util.go └── tincstat.go /.gitignore: -------------------------------------------------------------------------------- 1 | uptime-server 2 | tincstat 3 | *.swp 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | go: 4 | - 1.0 5 | - 1.1 6 | - 1.2 7 | - tip 8 | 9 | script: go test 10 | 11 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "net/http" 6 | ) 7 | 8 | var ( 9 | outputFile string 10 | ) 11 | 12 | func main() { 13 | http.HandleFunc("/tincstat", tincStatServer) 14 | err := http.ListenAndServe("127.0.0.1:9000", nil) 15 | if err != nil { 16 | log.Fatal("ListenAndServe: " + err.Error()) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Tinc Status Server 2 | 3 | ## API Usage 4 | 5 | ### Uptime 6 | 7 | Get some basics of the tinc daemon, must be run as the user of tinc 8 | 9 | Resource: /tincstat 10 | Method: GET 11 | 12 | #### Curl Example 13 | ``` 14 | curl -i http://127.0.0.1:9000/tincstat 15 | ``` 16 | 17 | Response 18 | 19 | ``` 20 | HTTP/1.1 200 OK 21 | Content-Type: application/json 22 | Date: Tue, 17 Dec 2013 00:57:36 GMT 23 | Content-Length: 111 24 | 25 | { 26 | "total_bytes_in": 115324, 27 | "total_bytes_out": 67990, 28 | "connections": [ 29 | { 30 | "name": "some_random_node", 31 | "ip": "192.0.2.15", 32 | "port": 2003 33 | } 34 | ] 35 | } 36 | 37 | ``` 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013 Kelsey Hightower 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 7 | of the Software, and to permit persons to whom the Software is furnished to do 8 | so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /util.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "strconv" 5 | "strings" 6 | ) 7 | 8 | // parseTincStat creates an object out of tinc log lines 9 | func parseTincStat(loglines []string) (*TincStat, error) { 10 | // Handle Total Bytes 11 | bytes_in_line := strings.Fields(loglines[1]) 12 | bytes_out_line := strings.Fields(loglines[2]) 13 | 14 | totalbytesin, _ := strconv.ParseInt(bytes_in_line[len(bytes_in_line)-1], 10, 64) 15 | totalbytesout, _ := strconv.ParseInt(bytes_out_line[len(bytes_out_line)-1], 10, 64) 16 | 17 | 18 | // Find and Parse Connections List 19 | var connections []TincConnection 20 | inside_connections_stanza := false 21 | for _, line := range loglines { 22 | if strings.Contains(line, "Connections:") { 23 | inside_connections_stanza = true 24 | continue 25 | } 26 | if strings.Contains(line, "End of connections.") { 27 | inside_connections_stanza = false 28 | break 29 | } 30 | if inside_connections_stanza { 31 | fields := strings.Fields(line) 32 | port, _ := strconv.ParseInt(fields[7], 10, 64) 33 | tinc_conn := TincConnection { 34 | Name: fields[3], 35 | Ip: fields[5], 36 | Port: port, 37 | } 38 | connections = append(connections, tinc_conn) 39 | } 40 | 41 | 42 | 43 | } 44 | 45 | ts := &TincStat { 46 | TotalBytesIn: totalbytesin, 47 | TotalBytesOut: totalbytesout, 48 | Connections: connections, 49 | } 50 | return ts, nil 51 | } 52 | -------------------------------------------------------------------------------- /tincstat.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "encoding/json" 6 | "errors" 7 | "fmt" 8 | "github.com/mitchellh/go-ps" 9 | "io" 10 | "log" 11 | "net/http" 12 | "os" 13 | "syscall" 14 | ) 15 | // TincStat represents the status of the tinc daemon 16 | 17 | type TincStat struct { 18 | TotalBytesIn int64 `json:"total_bytes_in"` 19 | TotalBytesOut int64 `json:"total_bytes_out"` 20 | Connections []TincConnection `json:"connections"` 21 | } 22 | 23 | type TincConnection struct { 24 | Name string `json:"name"` 25 | Ip string `json:"ip"` 26 | Port int64 `json:"port"` 27 | } 28 | 29 | 30 | 31 | // findTincPid finds the process of the 'tincd' daemon 32 | // TODO: reading the pidfile might be smarter 33 | func findTincPid() (int, error) { 34 | 35 | procs, err := ps.Processes() 36 | if err != nil { 37 | log.Fatalf("findTincPid: %s", err) 38 | } 39 | 40 | for _, proc := range procs { 41 | 42 | if proc.Executable() == "tincd" { 43 | //fmt.Println("pid: ", proc.Pid()) 44 | //fmt.Println("ppid: ", proc.PPid()) 45 | //fmt.Println("name: ", proc.Executable()) 46 | //fmt.Println("raw: ", proc) 47 | return proc.Pid(), err 48 | } 49 | } 50 | return 0, errors.New("findTincPid: Pid not found, is tinc running?") 51 | } 52 | 53 | // readLines reads a whole file into memory 54 | // and returns a slice of its lines. 55 | func readLines(path string) ([]string, error) { 56 | file, err := os.Open(path) 57 | if err != nil { 58 | return nil, err 59 | } 60 | defer file.Close() 61 | 62 | var lines []string 63 | scanner := bufio.NewScanner(file) 64 | for scanner.Scan() { 65 | lines = append(lines, scanner.Text()) 66 | } 67 | return lines, scanner.Err() 68 | } 69 | 70 | // list_contains checks a list for a member 71 | func list_contains(member string, list []string) bool { 72 | 73 | for _,element := range list { 74 | // index is the index where we are 75 | // element is the element from someSlice for where we are 76 | if element == member { 77 | return true 78 | } 79 | } 80 | return false 81 | } 82 | 83 | // user12 kills a process with USR1 then USR2 84 | func usr12(pid int) () { 85 | syscall.Kill(pid, syscall.SIGUSR1) 86 | syscall.Kill(pid, syscall.SIGUSR2) 87 | } 88 | 89 | 90 | // tincStatServer serves data pulled from the tinc log file 91 | // Tinc logs connection and network information after getting a USR1 and USR2 92 | // The following output is current: 93 | // 94 | //{ 95 | // "total_bytes_in": 115324, 96 | // "total_bytes_out": 67990, 97 | // "connections": [ 98 | // { 99 | // "name": "some_random_node", 100 | // "ip": "192.0.2.15", 101 | // "port": 2003 102 | // } 103 | // ] 104 | //} 105 | func tincStatServer(w http.ResponseWriter, req *http.Request) { 106 | // Get tinc pid 107 | tincPid, err := findTincPid() 108 | if err != nil { 109 | log.Fatalf("findTincPid: %s", err) 110 | } 111 | 112 | // Get first list of lines 113 | lines1, err := readLines("/var/log/tinc/tinc.log") 114 | if err != nil { 115 | log.Fatalf("readLines: %s", err) 116 | } 117 | // Send signals 118 | usr12(tincPid) 119 | // Confirm flush of data to file 120 | syscall.Sync() 121 | // Get second list of lines 122 | lines2, err := readLines("/var/log/tinc/tinc.log") 123 | if err != nil { 124 | log.Fatalf("readLines: %s", err) 125 | } 126 | 127 | // Print out and save unique lines in the second set 128 | var loglines []string 129 | for i, line := range lines2 { 130 | if list_contains(line, lines1) == false { 131 | fmt.Println(i, line) 132 | loglines = append(loglines, line) 133 | } 134 | } 135 | 136 | // Convert the raw loglines output to a tincstat object 137 | ts, err := parseTincStat(loglines) 138 | if err != nil { 139 | w.WriteHeader(http.StatusInternalServerError) 140 | return 141 | } 142 | 143 | // Create the JSON representation of tinc status 144 | data, err := json.MarshalIndent(ts, " ", "") 145 | if err != nil { 146 | w.WriteHeader(http.StatusInternalServerError) 147 | return 148 | } 149 | 150 | // Write the HTTP response headers and body. 151 | w.Header().Set("Content-Type", "application/json") 152 | w.WriteHeader(http.StatusOK) 153 | io.WriteString(w, string(data)) 154 | } 155 | --------------------------------------------------------------------------------