├── whois ├── version.go ├── flags.go └── query.go ├── .travis.yml ├── Dockerfile ├── unsetenv.go ├── whois42d.socket ├── unsetenv_13.go ├── whois42d.service ├── Makefile ├── .gitignore ├── LICENSE ├── README.md └── server.go /whois/version.go: -------------------------------------------------------------------------------- 1 | package whois 2 | 3 | const VERSION = 1 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | go: 4 | - 1.3 5 | - tip 6 | script: true 7 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM scratch 2 | USER 99 3 | ADD whois42d / 4 | CMD ["/whois42d", "-registry", "/registry", "-port", "4343"] 5 | -------------------------------------------------------------------------------- /unsetenv.go: -------------------------------------------------------------------------------- 1 | // +build go1.4 2 | 3 | package main 4 | 5 | import "os" 6 | 7 | func unsetenv(key string) { 8 | os.Unsetenv(key) 9 | } 10 | -------------------------------------------------------------------------------- /whois42d.socket: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Whois dn42 daemon socket 3 | 4 | [Socket] 5 | ListenStream=43 6 | 7 | [Install] 8 | WantedBy=sockets.target 9 | -------------------------------------------------------------------------------- /unsetenv_13.go: -------------------------------------------------------------------------------- 1 | // +build !go1.4 2 | 3 | package main 4 | 5 | import "os" 6 | 7 | func unsetenv(key string) { 8 | os.Setenv(key, "") 9 | } 10 | -------------------------------------------------------------------------------- /whois42d.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Whois dn42 daemon 3 | 4 | [Service] 5 | ExecStart=/usr/local/bin/whois42d -registry /var/lib/whois42d/registry 6 | User=nobody 7 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | VERSION?=1.0 2 | 3 | container: 4 | CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o whois42d . 5 | strip whois42d 6 | docker build -t mic92/whois42d:$(VERSION) . 7 | 8 | upload: 9 | docker tag -f mic92/whois42d:$(VERSION) mic92/whois42d:latest 10 | docker push mic92/whois42d 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | 10 | # Architecture specific extensions/prefixes 11 | *.[568vq] 12 | [568vq].out 13 | 14 | *.cgo1.go 15 | *.cgo2.c 16 | _cgo_defun.c 17 | _cgo_gotypes.go 18 | _cgo_export.* 19 | 20 | _testmain.go 21 | 22 | *.exe 23 | *.test 24 | *.prof 25 | 26 | whois42d 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Jörg Thalheim 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /whois/flags.go: -------------------------------------------------------------------------------- 1 | package whois 2 | 3 | import ( 4 | "flag" 5 | "strings" 6 | ) 7 | 8 | func shellSplit(s string) []string { 9 | split := strings.Split(s, " ") 10 | 11 | var result []string 12 | var inquote string 13 | var block string 14 | for _, i := range split { 15 | if inquote == "" { 16 | if strings.HasPrefix(i, "'") || strings.HasPrefix(i, "\"") { 17 | inquote = string(i[0]) 18 | block = strings.TrimPrefix(i, inquote) + " " 19 | } else { 20 | result = append(result, i) 21 | } 22 | } else { 23 | if !strings.HasSuffix(i, inquote) { 24 | block += i + " " 25 | } else { 26 | block += strings.TrimSuffix(i, inquote) 27 | inquote = "" 28 | result = append(result, block) 29 | block = "" 30 | } 31 | } 32 | } 33 | 34 | return result 35 | } 36 | 37 | type Flags struct { 38 | ServerInfo string 39 | TypeSchema string 40 | Types map[string]bool 41 | Args []string 42 | } 43 | 44 | func parseFlags(request string) (*Flags, *flag.FlagSet, error) { 45 | args := shellSplit(request) 46 | set := flag.NewFlagSet("whois42d", flag.ContinueOnError) 47 | var typeField string 48 | f := Flags{} 49 | set.StringVar(&f.ServerInfo, "q", "", "[version|sources|types] query specified server info") 50 | set.StringVar(&f.TypeSchema, "t", "", "request template for object of TYPE") 51 | set.StringVar(&typeField, "T", "", "TYPE[,TYPE]... only look for objects of TYPE") 52 | 53 | if err := set.Parse(args); err != nil { 54 | return nil, set, err 55 | } 56 | 57 | if typeField != "" { 58 | types := strings.Split(typeField, ",") 59 | f.Types = make(map[string]bool, len(types)) 60 | for _, t := range types { 61 | f.Types[t] = true 62 | } 63 | } 64 | 65 | f.Args = set.Args() 66 | return &f, set, nil 67 | } 68 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # whois42d 2 | [![Build Status](https://travis-ci.org/Mic92/whois42d.svg?branch=master)](https://travis-ci.org/Mic92/whois42d) 3 | 4 | Whois server for the dn42 registry. 5 | 6 | Based original on whoisd from the dn42 monotone registry written by welterde. 7 | 8 | ## Installation 9 | 10 | 1. Install a go compiler (like apt-get install go) 11 | 2. Setup Go Workspace: 12 | 13 | $ mkdir ~/go && export GOPATH=~/go 14 | 15 | 3. Download and install the daemon 16 | 17 | $ go get github.com/Mic92/whois42d 18 | 19 | ## Usage 20 | 21 | root> ~/go/bin/whois42d -registry /path/to/registry 22 | 23 | 24 | ## Run without root 25 | 26 | By default root privileges are required to run whois42d to be able to bind port 43. 27 | However you can use one of the following options to run whois42d without beeing root. 28 | 29 | 1. Use setcap on file: 30 | 31 | $ setcap 'cap_net_bind_service=+ep' ./whois42d 32 | 33 | 2. Use a supervisor supporting socket activation, for example systemd: 34 | 35 | $ cp whois42d.service whois42d.socket /etc/systemd/system 36 | $ install -D -m755 ~/go/bin/whois42d /usr/local/bin/ 37 | 38 | Edit whois42d.service to point to your monotone registry path, then enable it with 39 | 40 | $ systemctl enable whois42d.socket 41 | $ systemctl start whois42d.socket 42 | 43 | **NOTE**: Do not start whois42d.service directly (`systemctl start whois42d`), 44 | it run as user nobody, who cannot bind to port 43 itself. 45 | 46 | ## Supported Queries 47 | 48 | - mntner: `$ whois -h HAX404-MNT` 49 | - person: `$ whois -h HAX404-DN42` 50 | - aut-num: `$ whois -h AS4242420429` 51 | - dns: `$ whois -h hax404.dn42` 52 | - inetnum: `$ whois -h 172.23.136.0/23` or `$ whois -h 172.23.136.1` 53 | - inet6num: `$ whois -h fd58:eb75:347d::/48` 54 | - route: `$ whois -h 172.23.136.0/23` 55 | - route6: `$ whois -h fdec:1:1:dead::/64` 56 | - schema: `$ whois -h PERSON-SCHEMA` 57 | - organisation: `$ whois -h ORG-C3D2` 58 | - tinc-keyset: `$ whois -h SET-1-DN42-TINC` 59 | - tinc-key: `$ whois -h AS4242422703` 60 | - as-set: `$ whois -h 4242420000_4242423999` 61 | - as-block: `$ whois -h AS-FREIFUNK` 62 | - route-set: `$ whois -h RS-DN42-NATIVE` 63 | - version: `$ whois -h -q version` 64 | - sources: `$ whois -h -q sources` 65 | - types: `$ whois -h -q types` 66 | - type filtering: `$ whois -h 172.23.75.6 -T aut-num,person Mic92-DN42 AS4242420092` 67 | 68 | ## Run on docker 69 | 70 | There is also a pretty small container available (around 3mb). 71 | 72 | ``` 73 | $ docker run -v path/to/registry:/registry -p 43:4343 --rm mic92/whois42d 74 | ``` 75 | 76 | ## Build 77 | 78 | ``` 79 | go mod init github.com/Mic92/whois42d 80 | go mod tidy 81 | make 82 | ``` 83 | 84 | ## TODO 85 | 86 | - [ ] Match multiple objects by inverse index 87 | -------------------------------------------------------------------------------- /server.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "net" 7 | "os" 8 | "os/signal" 9 | "path" 10 | "strconv" 11 | "sync" 12 | "sync/atomic" 13 | "syscall" 14 | "time" 15 | 16 | "github.com/Mic92/whois42d/whois" 17 | ) 18 | 19 | type Server struct { 20 | registry whois.Registry 21 | LastConnection time.Time 22 | SocketActivation bool 23 | stopListening int32 24 | activeWorkers sync.WaitGroup 25 | } 26 | 27 | func New(dataPath string) *Server { 28 | registry := whois.Registry{dataPath} 29 | return &Server{registry, time.Now(), false, 0, sync.WaitGroup{}} 30 | } 31 | 32 | func (s *Server) Run(listener *net.TCPListener) { 33 | atomic.StoreInt32(&s.stopListening, 0) 34 | s.activeWorkers.Add(1) 35 | defer s.activeWorkers.Done() 36 | defer listener.Close() 37 | for atomic.LoadInt32(&s.stopListening) != 1 { 38 | if e := listener.SetDeadline(time.Now().Add(time.Second)); e != nil { 39 | fmt.Fprintf(os.Stderr, "Error setting deadline: %v\n", e) 40 | continue 41 | } 42 | conn, err := listener.AcceptTCP() 43 | if err != nil { 44 | if err, ok := err.(net.Error); ok && err.Timeout() { 45 | continue 46 | } else { 47 | fmt.Fprintf(os.Stderr, "Error: %v\n", err) 48 | os.Exit(1) 49 | } 50 | } 51 | 52 | s.activeWorkers.Add(1) 53 | s.LastConnection = time.Now() 54 | go s.handleConn(conn) 55 | } 56 | } 57 | 58 | func (s *Server) Shutdown() { 59 | atomic.StoreInt32(&s.stopListening, 1) 60 | s.activeWorkers.Wait() 61 | } 62 | 63 | func (s *Server) handleConn(conn *net.TCPConn) { 64 | defer func() { 65 | conn.Close() 66 | s.activeWorkers.Done() 67 | }() 68 | 69 | s.registry.HandleQuery(conn) 70 | } 71 | 72 | type options struct { 73 | Port uint 74 | Address string 75 | Registry string 76 | SocketTimeout float64 77 | } 78 | 79 | func parseFlags() options { 80 | var o options 81 | flag.UintVar(&o.Port, "port", 43, "port to listen") 82 | flag.StringVar(&o.Address, "address", "*", "address to listen") 83 | flag.StringVar(&o.Registry, "registry", ".", "path to dn42 registry") 84 | msg := "timeout in seconds before suspending the service when using socket activation" 85 | flag.Float64Var(&o.SocketTimeout, "timeout", 10, msg) 86 | flag.Parse() 87 | if o.Address == "*" { 88 | o.Address = "" 89 | } 90 | return o 91 | } 92 | 93 | func Listeners() []*net.TCPListener { 94 | defer unsetenv("LISTEN_PID") 95 | defer unsetenv("LISTEN_FDS") 96 | 97 | pid, err := strconv.Atoi(os.Getenv("LISTEN_PID")) 98 | if err != nil || pid != os.Getpid() { 99 | return nil 100 | } 101 | 102 | nfds, err := strconv.Atoi(os.Getenv("LISTEN_FDS")) 103 | if err != nil || nfds == 0 { 104 | return nil 105 | } 106 | 107 | listeners := make([]*net.TCPListener, 0) 108 | for fd := 3; fd < 3+nfds; fd++ { 109 | syscall.CloseOnExec(fd) 110 | file := os.NewFile(uintptr(fd), "LISTEN_FD_"+strconv.Itoa(fd)) 111 | if listener, err := net.FileListener(file); err == nil { 112 | if l, ok := listener.(*net.TCPListener); ok { 113 | listeners = append(listeners, l) 114 | } 115 | } 116 | } 117 | 118 | return listeners 119 | } 120 | 121 | func checkDataPath(registry string) (string, error) { 122 | dataPath := path.Join(registry, "data") 123 | 124 | if _, err := os.Stat(dataPath); err != nil { 125 | return "", fmt.Errorf("Cannot access '%s', should be in the registry repository: %s\n", 126 | dataPath, 127 | err) 128 | } 129 | return dataPath, nil 130 | } 131 | 132 | func createServer(opts options) (*Server, error) { 133 | dataPath, err := checkDataPath(opts.Registry) 134 | if err != nil { 135 | return nil, err 136 | } 137 | server := New(dataPath) 138 | 139 | if listeners := Listeners(); len(listeners) > 0 { 140 | fmt.Printf("socket action detected\n") 141 | server.SocketActivation = true 142 | for _, listener := range listeners { 143 | go server.Run(listener) 144 | } 145 | } else { 146 | address := opts.Address + ":" + strconv.Itoa(int(opts.Port)) 147 | listener, err := net.Listen("tcp", address) 148 | if err != nil { 149 | return nil, err 150 | } 151 | go server.Run(listener.(*net.TCPListener)) 152 | } 153 | return server, nil 154 | } 155 | 156 | func main() { 157 | opts := parseFlags() 158 | server, err := createServer(opts) 159 | if err != nil { 160 | fmt.Fprintf(os.Stderr, "Error: %v\n", err) 161 | os.Exit(1) 162 | } 163 | 164 | signals := make(chan os.Signal, 1) 165 | signal.Notify(signals, os.Interrupt) 166 | signal.Notify(signals, syscall.SIGTERM) 167 | signal.Notify(signals, syscall.SIGINT) 168 | 169 | if server.SocketActivation { 170 | Out: 171 | for { 172 | select { 173 | case <-signals: 174 | break Out 175 | case <-time.After(time.Second * 3): 176 | if time.Since(server.LastConnection).Seconds() >= opts.SocketTimeout { 177 | break Out 178 | } 179 | } 180 | } 181 | } else { 182 | <-signals 183 | } 184 | 185 | fmt.Printf("Shutting socket(s) down (takes up to 1s)\n") 186 | server.Shutdown() 187 | } 188 | -------------------------------------------------------------------------------- /whois/query.go: -------------------------------------------------------------------------------- 1 | package whois 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "flag" 7 | "fmt" 8 | "io/ioutil" 9 | "net" 10 | "os" 11 | "path" 12 | "regexp" 13 | "sort" 14 | "strings" 15 | ) 16 | 17 | type Registry struct { 18 | DataPath string 19 | } 20 | 21 | type Type struct { 22 | Name string 23 | Pattern *regexp.Regexp 24 | Kind int 25 | } 26 | 27 | type Object map[int]interface{} 28 | 29 | const ( 30 | UPPER = iota 31 | LOWER 32 | ROUTE 33 | ROUTE6 34 | ) 35 | 36 | type Query struct { 37 | Objects []Object 38 | Flags *Flags 39 | } 40 | 41 | var whoisTypes = []Type{ 42 | {"aut-num", regexp.MustCompile(`^AS([0123456789]+)$`), UPPER}, 43 | {"dns", regexp.MustCompile(`.dn42$`), LOWER}, 44 | {"person", regexp.MustCompile(`-DN42$`), UPPER}, 45 | {"person", regexp.MustCompile(`-NEONETWORK$`), UPPER}, 46 | {"mntner", regexp.MustCompile(`-MNT$`), UPPER}, 47 | {"schema", regexp.MustCompile(`-SCHEMA$`), UPPER}, 48 | {"organisation", regexp.MustCompile(`ORG-`), UPPER}, 49 | {"tinc-keyset", regexp.MustCompile(`^SET-.+-TINC$`), UPPER}, 50 | {"tinc-key", regexp.MustCompile(`-TINC$`), UPPER}, 51 | {"key-cert", regexp.MustCompile(`^PGPKEY-`), UPPER}, 52 | {"as-set", regexp.MustCompile(`^AS`), UPPER}, 53 | {"route-set", regexp.MustCompile(`^RS-`), UPPER}, 54 | {"inetnum", nil, ROUTE}, 55 | {"inet6num", nil, ROUTE6}, 56 | {"route", nil, ROUTE}, 57 | {"route6", nil, ROUTE6}, 58 | {"as-block", regexp.MustCompile(`\d+_\d+`), UPPER}, 59 | } 60 | 61 | func (r *Registry) handleObject(conn *net.TCPConn, object Object, flags *Flags) bool { 62 | found := false 63 | for _, t := range whoisTypes { 64 | if len(flags.Types) > 0 && !flags.Types[t.Name] { 65 | continue 66 | } 67 | 68 | if t.Kind == ROUTE || t.Kind == ROUTE6 { 69 | if object[t.Kind] != nil { 70 | if r.printNet(conn, t.Name, object[t.Kind].(net.IP)) { 71 | found = true 72 | } 73 | } 74 | } else { 75 | arg := object[t.Kind].(string) 76 | if t.Pattern.MatchString(arg) { 77 | r.printObject(conn, t.Name, arg) 78 | found = true 79 | } 80 | } 81 | } 82 | return found 83 | } 84 | 85 | func (r *Registry) HandleQuery(conn *net.TCPConn) { 86 | fmt.Fprint(conn, "% This is the dn42 whois query service.\n\n") 87 | 88 | query := parseQuery(conn) 89 | if query == nil { 90 | return 91 | } 92 | 93 | flags := query.Flags 94 | if flags.ServerInfo != "" { 95 | printServerInfo(conn, strings.TrimSpace(flags.ServerInfo)) 96 | return 97 | } 98 | found := false 99 | for _, obj := range query.Objects { 100 | if r.handleObject(conn, obj, flags) { 101 | found = true 102 | } 103 | } 104 | 105 | if !found { 106 | fmt.Fprint(conn, "% 404\n") 107 | } 108 | fmt.Fprint(conn, "\n") 109 | } 110 | 111 | func readCidrs(path string) ([]net.IPNet, error) { 112 | files, err := ioutil.ReadDir(path) 113 | if err != nil { 114 | return nil, err 115 | } 116 | cidrs := []net.IPNet{} 117 | for _, f := range files { 118 | name := strings.Replace(f.Name(), "_", "/", -1) 119 | _, cidr, err := net.ParseCIDR(name) 120 | if err != nil { 121 | fmt.Fprintf(os.Stderr, "skip invalid net '%s'", f.Name()) 122 | continue 123 | } 124 | i := sort.Search(len(cidrs), func(i int) bool { 125 | c := cidrs[i] 126 | return bytes.Compare(c.Mask, cidr.Mask) >= 0 127 | }) 128 | 129 | if i < len(cidrs) { 130 | cidrs = append(cidrs[:i], append([]net.IPNet{*cidr}, cidrs[i:]...)...) 131 | } else { 132 | cidrs = append(cidrs, *cidr) 133 | } 134 | } 135 | 136 | return cidrs, nil 137 | } 138 | 139 | func parseObject(arg string) Object { 140 | obj := path.Base(arg) 141 | object := Object{ 142 | UPPER: strings.ToUpper(obj), 143 | LOWER: strings.ToLower(obj), 144 | } 145 | 146 | ip := net.ParseIP(obj) 147 | if ip == nil { 148 | ip, _, _ = net.ParseCIDR(arg) 149 | } 150 | if ip != nil { 151 | if ip.To4() == nil { 152 | object[ROUTE6] = ip 153 | } else { 154 | object[ROUTE] = ip.To4() 155 | } 156 | } 157 | return object 158 | } 159 | 160 | func parseQuery(conn *net.TCPConn) *Query { 161 | r := bufio.NewReader(conn) 162 | req, e := r.ReadString('\n') 163 | if e != nil { 164 | fmt.Fprintf(os.Stderr, "Error: %v\n", e) 165 | return nil 166 | } 167 | flags, flagSet, err := parseFlags(req) 168 | if err != nil { 169 | flagSet.SetOutput(conn) 170 | if err != flag.ErrHelp { 171 | fmt.Fprintf(conn, "%s", err) 172 | } 173 | flagSet.PrintDefaults() 174 | return nil 175 | } 176 | 177 | query := Query{} 178 | query.Flags = flags 179 | query.Objects = make([]Object, len(flags.Args)) 180 | for i, arg := range flags.Args { 181 | query.Objects[i] = parseObject(strings.TrimSpace(arg)) 182 | } 183 | fmt.Fprintf(os.Stdout, "[%s] %s\n", conn.RemoteAddr(), req) 184 | return &query 185 | } 186 | 187 | func printServerInfo(conn *net.TCPConn, what string) { 188 | switch what { 189 | case "version": 190 | fmt.Fprintf(conn, "%% whois42d v%d\n", VERSION) 191 | case "sources": 192 | fmt.Fprintf(conn, "DN42:3:N:0-0\n") 193 | case "types": 194 | for _, t := range whoisTypes { 195 | fmt.Fprintf(conn, "%s\n", t.Name) 196 | } 197 | default: 198 | fmt.Fprintf(conn, "%% unknown option %s\n", what) 199 | } 200 | } 201 | 202 | func (r *Registry) printNet(conn *net.TCPConn, name string, ip net.IP) bool { 203 | routePath := path.Join(r.DataPath, name) 204 | cidrs, err := readCidrs(routePath) 205 | if err != nil { 206 | fmt.Printf("Error reading cidr from '%s'\n", routePath) 207 | } 208 | 209 | found := false 210 | for _, c := range cidrs { 211 | if c.Contains(ip) { 212 | obj := strings.Replace(c.String(), "/", "_", -1) 213 | r.printObject(conn, name, obj) 214 | found = true 215 | } 216 | } 217 | return found 218 | } 219 | 220 | func (r *Registry) printObject(conn *net.TCPConn, objType string, obj string) { 221 | file := path.Join(r.DataPath, objType, obj) 222 | 223 | f, err := os.Open(file) 224 | defer f.Close() 225 | if err != nil { 226 | if os.IsNotExist(err) { 227 | return 228 | } 229 | fmt.Fprintf(os.Stderr, "Error: %s\n", err) 230 | return 231 | } 232 | fmt.Fprintf(conn, "%% Information related to '%s':\n", file[len(r.DataPath)+1:]) 233 | conn.ReadFrom(f) 234 | fmt.Fprint(conn, "\n") 235 | } 236 | --------------------------------------------------------------------------------