├── LICENSE ├── README.md ├── cmd └── pingdns │ └── main.go ├── go.mod ├── go.sum └── pingdns.go /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2022 James Williams 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FRO, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # dns-over-ping(8) 2 | 3 | You've heard of DNS-over-HTTP, DNS-over-TLS, DNS-over-GRPC... Now get ready for 4 | DNS-over-ping(8)! 5 | 6 | Resolve names straight from the standard inetutils/iptools `ping` tool: 7 | 8 | ``` 9 | $ ping localhost -4 -p "$(printf "cloudflare.com?" | xxd -p -c0)" -c1 10 | PATTERN: 0x636c6f7564666c6172652e636f6d3f 11 | PING (127.0.0.1) 56(84) bytes of data. 12 | 72 bytes from localhost (127.0.0.1): icmp_seq=1 ttl=64 time=1667863702756 ms 13 | wrong data byte #16 should be 0x6c but was 0x68 14 | #16 68 10 85 e5 68 10 84 e5 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 15 | #48 0 0 0 0 0 0 0 0 16 | 17 | --- ping statistics --- 18 | 1 packets transmitted, 1 received, 0% packet loss, time 0ms 19 | rtt min/avg/max/mdev = 1667863702755.909/1667863702755.909/1667863702755.909/0.000 ms 20 | ``` 21 | 22 | Simply read your answer off from the `wrong data` hexdump: 23 | 24 | ``` 25 | 68 10 85 e5 68 10 84 e5 26 | => 0x68.0x10.0x85.0xe5 0x68.0x10.0x84.0xe5 27 | = 104.16.133.229 104.16.132.229 28 | ``` 29 | 30 | ### Limitations 31 | 32 | * Only `A` lookups supported, sorry. 33 | * Including both IPv4 and IPv6 addresses would necessitate a more 34 | complicated output format to avoid ambiguity. 35 | * The choice is then between IPv4 and IPv6 - IPv4 wins because we can 36 | display more IPs (`ping` will only show data 56 bytes in its errors). 37 | 38 | * Names can be at most 15 bytes long. 39 | * This is because `ping` only lets you specify 16 byte data patterns - 40 | everything beyond 16 bytes is ignored - and a byte more is required for 41 | the delimiter (question mark) 42 | 43 | * At most 14 IPs can be returned 44 | * `ping` will always display 56 bytes of hexdumped wrong data, regardless 45 | of how much is in the response packet 46 | * but, in fairness, I'd be surprised if any names resolve to more than 14 IPs 47 | 48 | None of these are inherent limitations of ICMP, rather they are limitations of 49 | the `ping` tool and its output. DNS-over-ICMP could actually be made to work 50 | pretty well (but would be less fun). 51 | 52 | ### Running 53 | 54 | Optionally, prevent your machine sending its own ICMP responses to incoming 55 | ICMP echo requests: 56 | 57 | ``` 58 | # echo "1" > /proc/sys/net/ipv4/icmp_echo_ignore_all 59 | ``` 60 | 61 | Then: 62 | 63 | ``` 64 | $ go build ./cmd/pingdns 65 | $ sudo setcap cap_net_raw+ep pingdns 66 | $ ./pingdns 67 | ``` 68 | 69 | Then, in another shell, for example: 70 | 71 | ``` 72 | $ ping localhost -4 -p "$(printf "jameswillia.ms?" | xxd -p -c0)" -c1 73 | ``` 74 | -------------------------------------------------------------------------------- /cmd/pingdns/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "log" 6 | 7 | pingdns "github.com/jamespwilliams/dns-over-ping" 8 | "go.uber.org/zap" 9 | "go.uber.org/zap/zapcore" 10 | ) 11 | 12 | func main() { 13 | logLevel := zap.LevelFlag("log-level", zap.InfoLevel, "one of DEBUG, INFO, WARN, ERROR. defaults to INFO") 14 | 15 | flag.Parse() 16 | 17 | config := zap.NewDevelopmentConfig() 18 | config.EncoderConfig.EncodeLevel = zapcore.CapitalColorLevelEncoder 19 | config.Level = zap.NewAtomicLevelAt(*logLevel) 20 | logger, err := config.Build() 21 | if err != nil { 22 | log.Fatalf("failed to build logger: %v", err) 23 | } 24 | 25 | panic(pingdns.NewServer(logger).Serve()) 26 | } 27 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/jamespwilliams/dns-over-ping 2 | 3 | go 1.19 4 | 5 | require ( 6 | github.com/google/gopacket v1.1.19 // indirect 7 | go.uber.org/atomic v1.10.0 // indirect 8 | go.uber.org/multierr v1.8.0 // indirect 9 | go.uber.org/zap v1.23.0 // indirect 10 | golang.org/x/net v0.1.0 // indirect 11 | golang.org/x/sys v0.1.0 // indirect 12 | ) 13 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 2 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF8= 4 | github.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo= 5 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 6 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 7 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 8 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 9 | go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= 10 | go.uber.org/atomic v1.10.0 h1:9qC72Qh0+3MqyJbAn8YU5xVq1frD8bn3JtD2oXtafVQ= 11 | go.uber.org/atomic v1.10.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= 12 | go.uber.org/multierr v1.8.0 h1:dg6GjLku4EH+249NNmoIciG9N/jURbDG+pFlTkhzIC8= 13 | go.uber.org/multierr v1.8.0/go.mod h1:7EAYxJLBy9rStEaz58O2t4Uvip6FSURkq8/ppBp95ak= 14 | go.uber.org/zap v1.23.0 h1:OjGQ5KQDEUawVHxNwQgPpiypGHOxo2mNZsOqTak4fFY= 15 | go.uber.org/zap v1.23.0/go.mod h1:D+nX8jyLsMHMYrln8A0rJjFt/T/9/bGgIhAqxv5URuY= 16 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 17 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 18 | golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= 19 | golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= 20 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 21 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 22 | golang.org/x/net v0.1.0 h1:hZ/3BUoy5aId7sCpA/Tc5lt8DkFgdVS2onTpJsZ/fl0= 23 | golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= 24 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 25 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 26 | golang.org/x/sys v0.0.0-20190412213103-97732733099d h1:+R4KGOnez64A81RvjARKc4UT5/tI9ujCIVX+P5KiHuI= 27 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 28 | golang.org/x/sys v0.1.0 h1:kunALQeHf1/185U1i0GOB/fy1IPRDDpuoOOqRReG57U= 29 | golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 30 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 31 | golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 32 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 33 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 34 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 35 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 36 | -------------------------------------------------------------------------------- /pingdns.go: -------------------------------------------------------------------------------- 1 | package pingdns 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "net" 8 | 9 | "go.uber.org/zap" 10 | "golang.org/x/net/icmp" 11 | "golang.org/x/net/ipv4" 12 | ) 13 | 14 | const ( 15 | icmpv4ChecksumLength = 16 16 | 17 | pingMinimumDataLength = 64 18 | pingLeadingDataLength = 16 19 | 20 | defaultSnaplen = 1600 21 | defaultNetwork = "ip4:icmp" 22 | defaultAddress = "0.0.0.0" 23 | ) 24 | 25 | type Server struct { 26 | snaplen int 27 | network, address string 28 | logger *zap.Logger 29 | } 30 | 31 | func NewServer(logger *zap.Logger) Server { 32 | return Server{ 33 | snaplen: defaultSnaplen, network: defaultNetwork, address: defaultAddress, logger: logger, 34 | } 35 | } 36 | 37 | func (s Server) WithSnaplen(snaplen int) { 38 | s.snaplen = snaplen 39 | } 40 | 41 | func (s Server) WithNetwork(network string) { 42 | s.network = network 43 | } 44 | 45 | func (s Server) Address(address string) { 46 | s.address = address 47 | } 48 | 49 | func (s Server) Serve() error { 50 | conn, err := icmp.ListenPacket(s.network, s.address) 51 | if err != nil { 52 | return fmt.Errorf("doicmp: listening failed: %w", err) 53 | } 54 | defer conn.Close() 55 | 56 | for { 57 | request := make([]byte, s.snaplen) 58 | 59 | n, addr, err := conn.ReadFrom(request) 60 | if err != nil { 61 | return fmt.Errorf("doicmp: reading packets failed: %w", err) 62 | } 63 | 64 | response, err := s.handleBytes(request[:n]) 65 | if err != nil { 66 | s.logger.Warn("failed to handle request", zap.Error(err)) 67 | continue 68 | } 69 | 70 | if response == nil { 71 | continue 72 | } 73 | 74 | if _, err := conn.WriteTo(response, addr); err != nil { 75 | s.logger.Warn("failed to write response", zap.Error(err)) 76 | } 77 | } 78 | } 79 | 80 | func (s Server) handleBytes(requestBytes []byte) (response []byte, err error) { 81 | parsed, err := icmp.ParseMessage(ipv4.ICMPTypeEcho.Protocol(), requestBytes) 82 | if err != nil { 83 | return nil, fmt.Errorf("doicmp: parsing message failed: %w", err) 84 | } 85 | 86 | switch parsed.Type { 87 | case ipv4.ICMPTypeEcho: 88 | default: 89 | return nil, nil 90 | } 91 | 92 | icmpEchoRequest, ok := parsed.Body.(*icmp.Echo) 93 | if !ok { 94 | return nil, errors.New("packet wasn't icmp echo?") 95 | } 96 | 97 | icmpMessageResponse, err := s.handleICMPEcho(icmpEchoRequest) 98 | if err != nil { 99 | return nil, err 100 | } 101 | 102 | return icmpMessageResponse.Marshal(nil) 103 | } 104 | 105 | func (s Server) handleICMPEcho(request *icmp.Echo) (response *icmp.Message, err error) { 106 | if l := len(request.Data); l < icmpv4ChecksumLength { 107 | return nil, fmt.Errorf("icmp echo data was too short? got %v bytes, expected at least %v", 108 | l, icmpv4ChecksumLength) 109 | } 110 | 111 | name, err := s.extractNameFromPayload(request.Data[icmpv4ChecksumLength:]) 112 | if err != nil { 113 | return nil, fmt.Errorf("failed to extract name from icmp payload: %w", err) 114 | } 115 | 116 | ipv4s, err := net.DefaultResolver.LookupIP(context.Background(), "ip4", name) 117 | if err != nil { 118 | return nil, fmt.Errorf("failed to resolve name: %w", err) 119 | } 120 | 121 | return &icmp.Message{ 122 | Type: ipv4.ICMPTypeEchoReply, 123 | Code: 0, 124 | Body: &icmp.Echo{ 125 | ID: request.ID, 126 | Seq: request.Seq, 127 | Data: prepareResponseData(ipv4s), 128 | }, 129 | }, nil 130 | } 131 | 132 | func prepareResponseData(ipv4s []net.IP) []byte { 133 | // When ping presents the "wrong data" message, it chops off `pingLeadingDataLength` 134 | // of leading data. Pad that out with zeroes, so our IPs get shown: 135 | responseData := make([]byte, pingLeadingDataLength) 136 | responseData = append(responseData, flatten(ipv4sToByteSlices(ipv4s))...) 137 | 138 | padding := make([]byte, pingMinimumDataLength-len(responseData)) 139 | responseData = append(responseData, padding...) 140 | 141 | return responseData 142 | } 143 | 144 | func (s Server) extractNameFromPayload(payload []byte) (string, error) { 145 | s.logger.Debug("extracting request name from payload", zap.String("payload", string(payload))) 146 | 147 | start := findIndex(payload, '?') 148 | if start == -1 { 149 | return "", errors.New("failed to find ? delimiter in icmp echo request data") 150 | } 151 | 152 | end := findIndex(payload[start+1:], '?') 153 | if end == -1 { 154 | return "", errors.New("failed to find ? delimiter in icmp echo request data") 155 | } 156 | 157 | name := string(payload[start+1 : start+1+end]) 158 | 159 | s.logger.Debug("got request name from payload", zap.String("name", name)) 160 | 161 | return name, nil 162 | } 163 | 164 | func findIndex(bytes []byte, delim byte) int { 165 | for i := range bytes { 166 | if bytes[i] == delim { 167 | return i 168 | } 169 | } 170 | return -1 171 | } 172 | 173 | func flatten(slices [][]byte) []byte { 174 | var res []byte 175 | for _, slice := range slices { 176 | res = append(res, slice...) 177 | } 178 | return res 179 | } 180 | 181 | func ipv4sToByteSlices(ips []net.IP) [][]byte { 182 | var slices [][]byte 183 | for _, ip := range ips { 184 | if ip == nil || len(ip) != 16 { 185 | continue 186 | } 187 | 188 | slices = append(slices, ip[12:]) 189 | } 190 | return slices 191 | } 192 | --------------------------------------------------------------------------------