├── .github └── dependabot.yml ├── .gitignore ├── LICENSE ├── README.md ├── dnsstamps.go ├── dnsstamps_test.go └── go.mod /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: gomod 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | time: "04:00" 8 | open-pull-requests-limit: 10 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.dll 4 | *.so 5 | *.dylib 6 | 7 | # Test binary, build with `go test -c` 8 | *.test 9 | 10 | # Output of the go coverage tool, specifically when used with LiteIDE 11 | *.out 12 | 13 | # Project-local glide cache, RE: https://github.com/Masterminds/glide/issues/736 14 | .glide/ 15 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018-2023 Frank Denis 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # go-dnsstamps 2 | 3 | DNS Stamps library for Go. 4 | 5 | ## [Documentation](https://pkg.go.dev/github.com/jedisct1/go-dnsstamps) 6 | -------------------------------------------------------------------------------- /dnsstamps.go: -------------------------------------------------------------------------------- 1 | package dnsstamps 2 | 3 | import ( 4 | "encoding/base64" 5 | "encoding/binary" 6 | "encoding/hex" 7 | "errors" 8 | "fmt" 9 | "net" 10 | "strconv" 11 | "strings" 12 | ) 13 | 14 | const ( 15 | DefaultPort = 443 16 | DefaultDNSPort = 53 17 | StampScheme = "sdns://" 18 | ) 19 | 20 | type ServerInformalProperties uint64 21 | 22 | const ( 23 | ServerInformalPropertyDNSSEC = ServerInformalProperties(1) << 0 24 | ServerInformalPropertyNoLog = ServerInformalProperties(1) << 1 25 | ServerInformalPropertyNoFilter = ServerInformalProperties(1) << 2 26 | ) 27 | 28 | type StampProtoType uint8 29 | 30 | const ( 31 | StampProtoTypePlain = StampProtoType(0x00) 32 | StampProtoTypeDNSCrypt = StampProtoType(0x01) 33 | StampProtoTypeDoH = StampProtoType(0x02) 34 | StampProtoTypeTLS = StampProtoType(0x03) 35 | StampProtoTypeDoQ = StampProtoType(0x04) 36 | StampProtoTypeODoHTarget = StampProtoType(0x05) 37 | StampProtoTypeDNSCryptRelay = StampProtoType(0x81) 38 | StampProtoTypeODoHRelay = StampProtoType(0x85) 39 | ) 40 | 41 | func (stampProtoType *StampProtoType) String() string { 42 | switch *stampProtoType { 43 | case StampProtoTypePlain: 44 | return "Plain" 45 | case StampProtoTypeDNSCrypt: 46 | return "DNSCrypt" 47 | case StampProtoTypeDoH: 48 | return "DoH" 49 | case StampProtoTypeTLS: 50 | return "TLS" 51 | case StampProtoTypeDoQ: 52 | return "QUIC" 53 | case StampProtoTypeODoHTarget: 54 | return "oDoH target" 55 | case StampProtoTypeDNSCryptRelay: 56 | return "DNSCrypt relay" 57 | case StampProtoTypeODoHRelay: 58 | return "oDoH relay" 59 | default: 60 | return "(unknown)" 61 | } 62 | } 63 | 64 | type ServerStamp struct { 65 | ServerAddrStr string 66 | ServerPk []uint8 67 | Hashes [][]uint8 68 | ProviderName string 69 | Path string 70 | Props ServerInformalProperties 71 | Proto StampProtoType 72 | } 73 | 74 | func NewDNSCryptServerStampFromLegacy(serverAddrStr string, serverPkStr string, providerName string, props ServerInformalProperties) (ServerStamp, error) { 75 | if net.ParseIP(serverAddrStr) != nil { 76 | serverAddrStr = fmt.Sprintf("%s:%d", serverAddrStr, DefaultPort) 77 | } 78 | serverPk, err := hex.DecodeString(strings.Replace(serverPkStr, ":", "", -1)) 79 | if err != nil || len(serverPk) != 32 { 80 | return ServerStamp{}, fmt.Errorf("Unsupported public key: [%s]", serverPkStr) 81 | } 82 | return ServerStamp{ 83 | ServerAddrStr: serverAddrStr, 84 | ServerPk: serverPk, 85 | ProviderName: providerName, 86 | Props: props, 87 | Proto: StampProtoTypeDNSCrypt, 88 | }, nil 89 | } 90 | 91 | func NewServerStampFromString(stampStr string) (ServerStamp, error) { 92 | if !strings.HasPrefix(stampStr, "sdns:") { 93 | return ServerStamp{}, errors.New("Stamps are expected to start with \"sdns:\"") 94 | } 95 | stampStr = stampStr[5:] 96 | stampStr = strings.TrimPrefix(stampStr, "//") 97 | bin, err := base64.RawURLEncoding.Strict().DecodeString(stampStr) 98 | if err != nil { 99 | return ServerStamp{}, err 100 | } 101 | if len(bin) < 1 { 102 | return ServerStamp{}, errors.New("Stamp is too short") 103 | } 104 | 105 | if bin[0] == uint8(StampProtoTypePlain) { 106 | return newPlainDNSServerStamp(bin) 107 | } else if bin[0] == uint8(StampProtoTypeDNSCrypt) { 108 | return newDNSCryptServerStamp(bin) 109 | } else if bin[0] == uint8(StampProtoTypeDoH) { 110 | return newDoHServerStamp(bin) 111 | } else if bin[0] == uint8(StampProtoTypeODoHTarget) { 112 | return newODoHTargetStamp(bin) 113 | } else if bin[0] == uint8(StampProtoTypeDNSCryptRelay) { 114 | return newDNSCryptRelayStamp(bin) 115 | } else if bin[0] == uint8(StampProtoTypeODoHRelay) { 116 | return newODoHRelayStamp(bin) 117 | } 118 | return ServerStamp{}, errors.New("Unsupported stamp version or protocol") 119 | } 120 | 121 | func NewRelayAndServerStampFromString(stampStr string) (ServerStamp, ServerStamp, error) { 122 | if !strings.HasPrefix(stampStr, StampScheme) { 123 | return ServerStamp{}, ServerStamp{}, errors.New("Stamps are expected to start with \"sdns://\"") 124 | } 125 | stampStr = stampStr[7:] 126 | parts := strings.Split(stampStr, "/") 127 | if len(parts) != 2 { 128 | return ServerStamp{}, ServerStamp{}, errors.New("This is not a relay+server stamp") 129 | } 130 | relayStamp, err := NewServerStampFromString(StampScheme + parts[0]) 131 | if err != nil { 132 | return ServerStamp{}, ServerStamp{}, err 133 | } 134 | serverStamp, err := NewServerStampFromString(StampScheme + parts[1]) 135 | if err != nil { 136 | return ServerStamp{}, ServerStamp{}, err 137 | } 138 | if relayStamp.Proto != StampProtoTypeDNSCryptRelay && relayStamp.Proto != StampProtoTypeODoHRelay { 139 | return ServerStamp{}, ServerStamp{}, errors.New("First stamp is not a relay") 140 | } 141 | if !(serverStamp.Proto != StampProtoTypeDNSCryptRelay && serverStamp.Proto != StampProtoTypeODoHRelay) { 142 | return ServerStamp{}, ServerStamp{}, errors.New("Second stamp is a relay") 143 | } 144 | return relayStamp, serverStamp, nil 145 | } 146 | 147 | // id(u8)=0x00 props 0x00 addrLen(1) serverAddr 148 | func newPlainDNSServerStamp(bin []byte) (ServerStamp, error) { 149 | stamp := ServerStamp{Proto: StampProtoTypePlain} 150 | if len(bin) < 1+8+1+1 { 151 | return stamp, errors.New("Stamp is too short") 152 | } 153 | stamp.Props = ServerInformalProperties(binary.LittleEndian.Uint64(bin[1:9])) 154 | binLen := len(bin) 155 | pos := 9 156 | 157 | length := int(bin[pos]) 158 | if 1+length > binLen-pos { 159 | return stamp, errors.New("Invalid stamp") 160 | } 161 | pos++ 162 | stamp.ServerAddrStr = string(bin[pos : pos+length]) 163 | pos += length 164 | 165 | colIndex := strings.LastIndex(stamp.ServerAddrStr, ":") 166 | bracketIndex := strings.LastIndex(stamp.ServerAddrStr, "]") 167 | if colIndex < bracketIndex { 168 | colIndex = -1 169 | } 170 | if colIndex < 0 { 171 | colIndex = len(stamp.ServerAddrStr) // DefaultDNSPort 172 | stamp.ServerAddrStr = fmt.Sprintf("%s:%d", stamp.ServerAddrStr, DefaultDNSPort) 173 | } 174 | if colIndex >= len(stamp.ServerAddrStr)-1 { 175 | return stamp, errors.New("Invalid stamp (empty port)") 176 | } 177 | ipOnly := stamp.ServerAddrStr[:colIndex] 178 | if err := validatePort(stamp.ServerAddrStr[colIndex+1:]); err != nil { 179 | return stamp, errors.New("Invalid stamp (port range)") 180 | } 181 | if net.ParseIP(strings.TrimRight(strings.TrimLeft(ipOnly, "["), "]")) == nil { 182 | return stamp, errors.New("Invalid stamp (IP address)") 183 | } 184 | if pos != binLen { 185 | return stamp, errors.New("Invalid stamp (garbage after end)") 186 | } 187 | return stamp, nil 188 | } 189 | 190 | // id(u8)=0x01 props addrLen(1) serverAddr pkStrlen(1) pkStr providerNameLen(1) providerName 191 | 192 | func newDNSCryptServerStamp(bin []byte) (ServerStamp, error) { 193 | stamp := ServerStamp{Proto: StampProtoTypeDNSCrypt} 194 | if len(bin) < 66 { 195 | return stamp, errors.New("Stamp is too short") 196 | } 197 | stamp.Props = ServerInformalProperties(binary.LittleEndian.Uint64(bin[1:9])) 198 | binLen := len(bin) 199 | pos := 9 200 | 201 | length := int(bin[pos]) 202 | if 1+length >= binLen-pos { 203 | return stamp, errors.New("Invalid stamp") 204 | } 205 | pos++ 206 | stamp.ServerAddrStr = string(bin[pos : pos+length]) 207 | pos += length 208 | 209 | colIndex := strings.LastIndex(stamp.ServerAddrStr, ":") 210 | bracketIndex := strings.LastIndex(stamp.ServerAddrStr, "]") 211 | if colIndex < bracketIndex { 212 | colIndex = -1 213 | } 214 | if colIndex < 0 { 215 | colIndex = len(stamp.ServerAddrStr) 216 | stamp.ServerAddrStr = fmt.Sprintf("%s:%d", stamp.ServerAddrStr, DefaultPort) 217 | } 218 | if colIndex >= len(stamp.ServerAddrStr)-1 { 219 | return stamp, errors.New("Invalid stamp (empty port)") 220 | } 221 | ipOnly := stamp.ServerAddrStr[:colIndex] 222 | if err := validatePort(stamp.ServerAddrStr[colIndex+1:]); err != nil { 223 | return stamp, errors.New("Invalid stamp (port range)") 224 | } 225 | if net.ParseIP(strings.TrimRight(strings.TrimLeft(ipOnly, "["), "]")) == nil { 226 | return stamp, errors.New("Invalid stamp (IP address)") 227 | } 228 | 229 | length = int(bin[pos]) 230 | if 1+length >= binLen-pos { 231 | return stamp, errors.New("Invalid stamp") 232 | } 233 | pos++ 234 | stamp.ServerPk = bin[pos : pos+length] 235 | pos += length 236 | 237 | length = int(bin[pos]) 238 | if length >= binLen-pos { 239 | return stamp, errors.New("Invalid stamp") 240 | } 241 | pos++ 242 | stamp.ProviderName = string(bin[pos : pos+length]) 243 | pos += length 244 | 245 | if pos != binLen { 246 | return stamp, errors.New("Invalid stamp (garbage after end)") 247 | } 248 | return stamp, nil 249 | } 250 | 251 | // id(u8)=0x02 props addrLen(1) serverAddr hashLen(1) hash hostNameLen(1) hostName pathLen(1) path 252 | 253 | func newDoHServerStamp(bin []byte) (ServerStamp, error) { 254 | stamp := ServerStamp{Proto: StampProtoTypeDoH} 255 | if len(bin) < 15 { 256 | return stamp, errors.New("Stamp is too short") 257 | } 258 | stamp.Props = ServerInformalProperties(binary.LittleEndian.Uint64(bin[1:9])) 259 | binLen := len(bin) 260 | pos := 9 261 | 262 | length := int(bin[pos]) 263 | if 1+length >= binLen-pos { 264 | return stamp, errors.New("Invalid stamp") 265 | } 266 | pos++ 267 | stamp.ServerAddrStr = string(bin[pos : pos+length]) 268 | pos += length 269 | 270 | for { 271 | vlen := int(bin[pos]) 272 | length = vlen & ^0x80 273 | if 1+length >= binLen-pos { 274 | return stamp, errors.New("Invalid stamp") 275 | } 276 | pos++ 277 | if length > 0 { 278 | stamp.Hashes = append(stamp.Hashes, bin[pos:pos+length]) 279 | } 280 | pos += length 281 | if vlen&0x80 != 0x80 { 282 | break 283 | } 284 | } 285 | 286 | length = int(bin[pos]) 287 | if 1+length >= binLen-pos { 288 | return stamp, errors.New("Invalid stamp") 289 | } 290 | pos++ 291 | stamp.ProviderName = string(bin[pos : pos+length]) 292 | pos += length 293 | 294 | length = int(bin[pos]) 295 | if length >= binLen-pos { 296 | return stamp, errors.New("Invalid stamp") 297 | } 298 | pos++ 299 | stamp.Path = string(bin[pos : pos+length]) 300 | pos += length 301 | 302 | if pos != binLen { 303 | return stamp, errors.New("Invalid stamp (garbage after end)") 304 | } 305 | 306 | if len(stamp.ServerAddrStr) > 0 { 307 | colIndex := strings.LastIndex(stamp.ServerAddrStr, ":") 308 | bracketIndex := strings.LastIndex(stamp.ServerAddrStr, "]") 309 | if colIndex < bracketIndex { 310 | colIndex = -1 311 | } 312 | if colIndex < 0 { 313 | colIndex = len(stamp.ServerAddrStr) 314 | stamp.ServerAddrStr = fmt.Sprintf("%s:%d", stamp.ServerAddrStr, DefaultPort) 315 | } 316 | if colIndex >= len(stamp.ServerAddrStr)-1 { 317 | return stamp, errors.New("Invalid stamp (empty port)") 318 | } 319 | ipOnly := stamp.ServerAddrStr[:colIndex] 320 | if err := validatePort(stamp.ServerAddrStr[colIndex+1:]); err != nil { 321 | return stamp, errors.New("Invalid stamp (port range)") 322 | } 323 | if net.ParseIP(strings.TrimRight(strings.TrimLeft(ipOnly, "["), "]")) == nil { 324 | return stamp, errors.New("Invalid stamp (IP address)") 325 | } 326 | } 327 | 328 | return stamp, nil 329 | } 330 | 331 | // id(u8)=0x05 props hostNameLen(1) hostName pathLen(1) path 332 | 333 | func newODoHTargetStamp(bin []byte) (ServerStamp, error) { 334 | stamp := ServerStamp{Proto: StampProtoTypeODoHTarget} 335 | if len(bin) < 12 { 336 | return stamp, errors.New("Stamp is too short") 337 | } 338 | stamp.Props = ServerInformalProperties(binary.LittleEndian.Uint64(bin[1:9])) 339 | binLen := len(bin) 340 | pos := 9 341 | 342 | length := int(bin[pos]) 343 | if 1+length >= binLen-pos { 344 | return stamp, errors.New("Invalid stamp") 345 | } 346 | pos++ 347 | stamp.ProviderName = string(bin[pos : pos+length]) 348 | pos += length 349 | 350 | length = int(bin[pos]) 351 | if length >= binLen-pos { 352 | return stamp, errors.New("Invalid stamp") 353 | } 354 | pos++ 355 | stamp.Path = string(bin[pos : pos+length]) 356 | pos += length 357 | 358 | if pos != binLen { 359 | return stamp, errors.New("Invalid stamp (garbage after end)") 360 | } 361 | 362 | return stamp, nil 363 | } 364 | 365 | // id(u8)=0x81 addrLen(1) serverAddr 366 | 367 | func newDNSCryptRelayStamp(bin []byte) (ServerStamp, error) { 368 | stamp := ServerStamp{Proto: StampProtoTypeDNSCryptRelay} 369 | if len(bin) < 9 { 370 | return stamp, errors.New("Stamp is too short") 371 | } 372 | binLen := len(bin) 373 | pos := 1 374 | length := int(bin[pos]) 375 | if 1+length > binLen-pos { 376 | return stamp, errors.New("Invalid stamp") 377 | } 378 | pos++ 379 | stamp.ServerAddrStr = string(bin[pos : pos+length]) 380 | pos += length 381 | 382 | colIndex := strings.LastIndex(stamp.ServerAddrStr, ":") 383 | bracketIndex := strings.LastIndex(stamp.ServerAddrStr, "]") 384 | if colIndex < bracketIndex { 385 | colIndex = -1 386 | } 387 | if colIndex < 0 { 388 | colIndex = len(stamp.ServerAddrStr) 389 | stamp.ServerAddrStr = fmt.Sprintf("%s:%d", stamp.ServerAddrStr, DefaultPort) 390 | } 391 | if colIndex >= len(stamp.ServerAddrStr)-1 { 392 | return stamp, errors.New("Invalid stamp (empty port)") 393 | } 394 | ipOnly := stamp.ServerAddrStr[:colIndex] 395 | if err := validatePort(stamp.ServerAddrStr[colIndex+1:]); err != nil { 396 | return stamp, errors.New("Invalid stamp (port range)") 397 | } 398 | if net.ParseIP(strings.TrimRight(strings.TrimLeft(ipOnly, "["), "]")) == nil { 399 | return stamp, errors.New("Invalid stamp (IP address)") 400 | } 401 | if pos != binLen { 402 | return stamp, errors.New("Invalid stamp (garbage after end)") 403 | } 404 | return stamp, nil 405 | } 406 | 407 | // id(u8)=0x85 props addrLen(1) serverAddr hashLen(1) hash hostNameLen(1) hostName pathLen(1) path 408 | 409 | func newODoHRelayStamp(bin []byte) (ServerStamp, error) { 410 | stamp := ServerStamp{Proto: StampProtoTypeODoHRelay} 411 | if len(bin) < 13 { 412 | return stamp, errors.New("Stamp is too short") 413 | } 414 | stamp.Props = ServerInformalProperties(binary.LittleEndian.Uint64(bin[1:9])) 415 | binLen := len(bin) 416 | pos := 9 417 | 418 | length := int(bin[pos]) 419 | if 1+length >= binLen-pos { 420 | return stamp, errors.New("Invalid stamp") 421 | } 422 | pos++ 423 | stamp.ServerAddrStr = string(bin[pos : pos+length]) 424 | pos += length 425 | 426 | for { 427 | vlen := int(bin[pos]) 428 | length = vlen & ^0x80 429 | if 1+length >= binLen-pos { 430 | return stamp, errors.New("Invalid stamp") 431 | } 432 | pos++ 433 | if length > 0 { 434 | stamp.Hashes = append(stamp.Hashes, bin[pos:pos+length]) 435 | } 436 | pos += length 437 | if vlen&0x80 != 0x80 { 438 | break 439 | } 440 | } 441 | 442 | length = int(bin[pos]) 443 | if 1+length >= binLen-pos { 444 | return stamp, errors.New("Invalid stamp") 445 | } 446 | pos++ 447 | stamp.ProviderName = string(bin[pos : pos+length]) 448 | pos += length 449 | 450 | length = int(bin[pos]) 451 | if length >= binLen-pos { 452 | return stamp, errors.New("Invalid stamp") 453 | } 454 | pos++ 455 | stamp.Path = string(bin[pos : pos+length]) 456 | pos += length 457 | 458 | if pos != binLen { 459 | return stamp, errors.New("Invalid stamp (garbage after end)") 460 | } 461 | 462 | if len(stamp.ServerAddrStr) > 0 { 463 | colIndex := strings.LastIndex(stamp.ServerAddrStr, ":") 464 | bracketIndex := strings.LastIndex(stamp.ServerAddrStr, "]") 465 | if colIndex < bracketIndex { 466 | colIndex = -1 467 | } 468 | if colIndex < 0 { 469 | colIndex = len(stamp.ServerAddrStr) 470 | stamp.ServerAddrStr = fmt.Sprintf("%s:%d", stamp.ServerAddrStr, DefaultPort) 471 | } 472 | if colIndex >= len(stamp.ServerAddrStr)-1 { 473 | return stamp, errors.New("Invalid stamp (empty port)") 474 | } 475 | ipOnly := stamp.ServerAddrStr[:colIndex] 476 | if err := validatePort(stamp.ServerAddrStr[colIndex+1:]); err != nil { 477 | return stamp, errors.New("Invalid stamp (port range)") 478 | } 479 | if net.ParseIP(strings.TrimRight(strings.TrimLeft(ipOnly, "["), "]")) == nil { 480 | return stamp, errors.New("Invalid stamp (IP address)") 481 | } 482 | } 483 | 484 | return stamp, nil 485 | } 486 | 487 | func validatePort(port string) error { 488 | if _, err := strconv.ParseUint(port, 10, 16); err != nil { 489 | return errors.New("Invalid port") 490 | } 491 | return nil 492 | } 493 | 494 | func (stamp *ServerStamp) String() string { 495 | if stamp.Proto == StampProtoTypePlain { 496 | return stamp.plainStrng() 497 | } else if stamp.Proto == StampProtoTypeDNSCrypt { 498 | return stamp.dnsCryptString() 499 | } else if stamp.Proto == StampProtoTypeDoH { 500 | return stamp.dohString() 501 | } else if stamp.Proto == StampProtoTypeODoHTarget { 502 | return stamp.oDohTargetString() 503 | } else if stamp.Proto == StampProtoTypeDNSCryptRelay { 504 | return stamp.dnsCryptRelayString() 505 | } else if stamp.Proto == StampProtoTypeODoHRelay { 506 | return stamp.oDohRelayString() 507 | } 508 | panic("Unsupported protocol") 509 | } 510 | 511 | func (stamp *ServerStamp) plainStrng() string { 512 | bin := make([]uint8, 9) 513 | bin[0] = uint8(StampProtoTypePlain) 514 | binary.LittleEndian.PutUint64(bin[1:9], uint64(stamp.Props)) 515 | 516 | serverAddrStr := stamp.ServerAddrStr 517 | if strings.HasSuffix(serverAddrStr, ":"+strconv.Itoa(DefaultDNSPort)) { 518 | serverAddrStr = serverAddrStr[:len(serverAddrStr)-1-len(strconv.Itoa(DefaultDNSPort))] 519 | } 520 | bin = append(bin, uint8(len(serverAddrStr))) 521 | bin = append(bin, []uint8(serverAddrStr)...) 522 | str := base64.RawURLEncoding.EncodeToString(bin) 523 | 524 | return StampScheme + str 525 | } 526 | 527 | func (stamp *ServerStamp) dnsCryptString() string { 528 | bin := make([]uint8, 9) 529 | bin[0] = uint8(StampProtoTypeDNSCrypt) 530 | binary.LittleEndian.PutUint64(bin[1:9], uint64(stamp.Props)) 531 | 532 | serverAddrStr := stamp.ServerAddrStr 533 | if strings.HasSuffix(serverAddrStr, ":"+strconv.Itoa(DefaultPort)) { 534 | serverAddrStr = serverAddrStr[:len(serverAddrStr)-1-len(strconv.Itoa(DefaultPort))] 535 | } 536 | bin = append(bin, uint8(len(serverAddrStr))) 537 | bin = append(bin, []uint8(serverAddrStr)...) 538 | 539 | bin = append(bin, uint8(len(stamp.ServerPk))) 540 | bin = append(bin, stamp.ServerPk...) 541 | 542 | bin = append(bin, uint8(len(stamp.ProviderName))) 543 | bin = append(bin, []uint8(stamp.ProviderName)...) 544 | 545 | str := base64.RawURLEncoding.EncodeToString(bin) 546 | 547 | return StampScheme + str 548 | } 549 | 550 | func (stamp *ServerStamp) dohString() string { 551 | bin := make([]uint8, 9) 552 | bin[0] = uint8(StampProtoTypeDoH) 553 | binary.LittleEndian.PutUint64(bin[1:9], uint64(stamp.Props)) 554 | 555 | serverAddrStr := stamp.ServerAddrStr 556 | if strings.HasSuffix(serverAddrStr, ":"+strconv.Itoa(DefaultPort)) { 557 | serverAddrStr = serverAddrStr[:len(serverAddrStr)-1-len(strconv.Itoa(DefaultPort))] 558 | } 559 | bin = append(bin, uint8(len(serverAddrStr))) 560 | bin = append(bin, []uint8(serverAddrStr)...) 561 | 562 | if len(stamp.Hashes) == 0 { 563 | bin = append(bin, uint8(0)) 564 | } else { 565 | last := len(stamp.Hashes) - 1 566 | for i, hash := range stamp.Hashes { 567 | vlen := len(hash) 568 | if i < last { 569 | vlen |= 0x80 570 | } 571 | bin = append(bin, uint8(vlen)) 572 | bin = append(bin, hash...) 573 | } 574 | } 575 | 576 | bin = append(bin, uint8(len(stamp.ProviderName))) 577 | bin = append(bin, []uint8(stamp.ProviderName)...) 578 | 579 | bin = append(bin, uint8(len(stamp.Path))) 580 | bin = append(bin, []uint8(stamp.Path)...) 581 | 582 | str := base64.RawURLEncoding.EncodeToString(bin) 583 | 584 | return StampScheme + str 585 | } 586 | 587 | func (stamp *ServerStamp) oDohTargetString() string { 588 | bin := make([]uint8, 9) 589 | bin[0] = uint8(StampProtoTypeODoHTarget) 590 | binary.LittleEndian.PutUint64(bin[1:9], uint64(stamp.Props)) 591 | 592 | bin = append(bin, uint8(len(stamp.ProviderName))) 593 | bin = append(bin, []uint8(stamp.ProviderName)...) 594 | 595 | bin = append(bin, uint8(len(stamp.Path))) 596 | bin = append(bin, []uint8(stamp.Path)...) 597 | 598 | str := base64.RawURLEncoding.EncodeToString(bin) 599 | 600 | return StampScheme + str 601 | } 602 | 603 | func (stamp *ServerStamp) dnsCryptRelayString() string { 604 | bin := make([]uint8, 1) 605 | bin[0] = uint8(StampProtoTypeDNSCryptRelay) 606 | 607 | serverAddrStr := stamp.ServerAddrStr 608 | if strings.HasSuffix(serverAddrStr, ":"+strconv.Itoa(DefaultPort)) { 609 | serverAddrStr = serverAddrStr[:len(serverAddrStr)-1-len(strconv.Itoa(DefaultPort))] 610 | } 611 | bin = append(bin, uint8(len(serverAddrStr))) 612 | bin = append(bin, []uint8(serverAddrStr)...) 613 | 614 | str := base64.RawURLEncoding.EncodeToString(bin) 615 | 616 | return StampScheme + str 617 | } 618 | 619 | func (stamp *ServerStamp) oDohRelayString() string { 620 | bin := make([]uint8, 9) 621 | bin[0] = uint8(StampProtoTypeODoHRelay) 622 | binary.LittleEndian.PutUint64(bin[1:9], uint64(stamp.Props)) 623 | 624 | serverAddrStr := stamp.ServerAddrStr 625 | if strings.HasSuffix(serverAddrStr, ":"+strconv.Itoa(DefaultPort)) { 626 | serverAddrStr = serverAddrStr[:len(serverAddrStr)-1-len(strconv.Itoa(DefaultPort))] 627 | } 628 | bin = append(bin, uint8(len(serverAddrStr))) 629 | bin = append(bin, []uint8(serverAddrStr)...) 630 | 631 | if len(stamp.Hashes) == 0 { 632 | bin = append(bin, uint8(0)) 633 | } else { 634 | last := len(stamp.Hashes) - 1 635 | for i, hash := range stamp.Hashes { 636 | vlen := len(hash) 637 | if i < last { 638 | vlen |= 0x80 639 | } 640 | bin = append(bin, uint8(vlen)) 641 | bin = append(bin, hash...) 642 | } 643 | } 644 | 645 | bin = append(bin, uint8(len(stamp.ProviderName))) 646 | bin = append(bin, []uint8(stamp.ProviderName)...) 647 | 648 | bin = append(bin, uint8(len(stamp.Path))) 649 | bin = append(bin, []uint8(stamp.Path)...) 650 | 651 | str := base64.RawURLEncoding.EncodeToString(bin) 652 | 653 | return StampScheme + str 654 | } 655 | -------------------------------------------------------------------------------- /dnsstamps_test.go: -------------------------------------------------------------------------------- 1 | package dnsstamps 2 | 3 | import ( 4 | "encoding/hex" 5 | "strings" 6 | "testing" 7 | ) 8 | 9 | var pk1 []byte 10 | 11 | func init() { 12 | var err error 13 | // generated with: 14 | // openssl x509 -noout -fingerprint -sha256 -inform pem -in /etc/ssl/certs/Go_Daddy_Class_2_CA.pem 15 | pkStr := "C3:84:6B:F2:4B:9E:93:CA:64:27:4C:0E:C6:7C:1E:CC:5E:02:4F:FC:AC:D2:D7:40:19:35:0E:81:FE:54:6A:E4" 16 | pk1, err = hex.DecodeString(strings.Replace(pkStr, ":", "", -1)) 17 | if err != nil { 18 | panic(err) 19 | } 20 | if len(pk1) != 32 { 21 | panic("invalid public key fingerprint") 22 | } 23 | } 24 | 25 | func TestDnscryptStamp(t *testing.T) { 26 | // same as exampleStamp in dnscrypt-stamper 27 | const expected = `sdns://AQcAAAAAAAAACTEyNy4wLjAuMSDDhGvyS56TymQnTA7GfB7MXgJP_KzS10AZNQ6B_lRq5BkyLmRuc2NyeXB0LWNlcnQubG9jYWxob3N0` 28 | 29 | var stamp ServerStamp 30 | stamp.Props |= ServerInformalPropertyDNSSEC 31 | stamp.Props |= ServerInformalPropertyNoLog 32 | stamp.Props |= ServerInformalPropertyNoFilter 33 | stamp.Proto = StampProtoTypeDNSCrypt 34 | stamp.ServerAddrStr = "127.0.0.1" 35 | 36 | stamp.ProviderName = "2.dnscrypt-cert.localhost" 37 | stamp.ServerPk = pk1 38 | stampStr := stamp.String() 39 | 40 | if stampStr != expected { 41 | t.Errorf("expected stamp %q but got instead %q", expected, stampStr) 42 | } 43 | 44 | parsedStamp, err := NewServerStampFromString(stampStr) 45 | if err != nil { 46 | t.Fatal(err) 47 | } 48 | ps := parsedStamp.String() 49 | if ps != stampStr { 50 | t.Errorf("re-parsed stamp string is %q, but %q expected", ps, stampStr) 51 | } 52 | } 53 | 54 | func TestDNSOverHTTP_NoHashes(t *testing.T) { 55 | const expected = `sdns://AgcAAAAAAAAACTEyNy4wLjAuMSDDhGvyS56TymQnTA7GfB7MXgJP_KzS10AZNQ6B_lRq5AtleGFtcGxlLmNvbQovZG5zLXF1ZXJ5` 56 | 57 | var stamp ServerStamp 58 | stamp.Props |= ServerInformalPropertyDNSSEC 59 | stamp.Props |= ServerInformalPropertyNoLog 60 | stamp.Props |= ServerInformalPropertyNoFilter 61 | stamp.ServerAddrStr = "127.0.0.1" 62 | 63 | stamp.Proto = StampProtoTypeDoH 64 | stamp.ProviderName = "example.com" 65 | stamp.Hashes = [][]uint8{pk1} 66 | stamp.Path = "/dns-query" 67 | stampStr := stamp.String() 68 | 69 | if stampStr != expected { 70 | t.Errorf("expected stamp %q but got instead %q", expected, stampStr) 71 | } 72 | 73 | parsedStamp, err := NewServerStampFromString(stampStr) 74 | if err != nil { 75 | t.Fatal(err) 76 | } 77 | ps := parsedStamp.String() 78 | if ps != stampStr { 79 | t.Errorf("re-parsed stamp string is %q, but %q expected", ps, stampStr) 80 | } 81 | } 82 | 83 | func TestDNSOverHTTP2_2(t *testing.T) { 84 | const q9 = `sdns://AgYAAAAAAAAACDkuOS45LjEwABJkbnM5LnF1YWQ5Lm5ldDo0NDMKL2Rucy1xdWVyeQ` 85 | 86 | parsedStamp, err := NewServerStampFromString(q9) 87 | if err != nil { 88 | t.Fatal(err) 89 | } 90 | ps := parsedStamp.String() 91 | if ps != q9 { 92 | t.Errorf("re-parsed stamp string is %q, but %q expected", ps, q9) 93 | } 94 | } 95 | 96 | func TestODoHTarget(t *testing.T) { 97 | const stamp = `sdns://BQcAAAAAAAAAEG9kb2guZXhhbXBsZS5jb20HL3RhcmdldA` 98 | 99 | parsedStamp, err := NewServerStampFromString(stamp) 100 | if err != nil { 101 | t.Fatal(err) 102 | } 103 | ps := parsedStamp.String() 104 | if ps != stamp { 105 | t.Errorf("re-parsed stamp string is %q, but %q expected", ps, stamp) 106 | } 107 | } 108 | 109 | func TestODoHRelay(t *testing.T) { 110 | const stamp = `sdns://hQcAAAAAAAAAB1s6OjFdOjGCq80CASMPZG9oLmV4YW1wbGUuY29tBi9yZWxheQ` 111 | 112 | parsedStamp, err := NewServerStampFromString(stamp) 113 | if err != nil { 114 | t.Fatal(err) 115 | } 116 | ps := parsedStamp.String() 117 | if ps != stamp { 118 | t.Errorf("re-parsed stamp string is %q, but %q expected", ps, stamp) 119 | } 120 | } 121 | 122 | func TestRelayServerPair(t *testing.T) { 123 | const stamp = `sdns://hQcAAAAAAAAAB1s6OjFdOjGCq80CASMPZG9oLmV4YW1wbGUuY29tBi9yZWxheQ/BQcAAAAAAAAAEG9kb2guZXhhbXBsZS5jb20HL3RhcmdldA` 124 | _, _, err := NewRelayAndServerStampFromString(stamp) 125 | if err != nil { 126 | t.Fatal(err) 127 | } 128 | } 129 | 130 | func TestPlainOldDNS(t *testing.T) { 131 | // [DNSSEC|No Filter|No Log] + 8.8.8.8 (no port) 132 | const stamp = `sdns://AAcAAAAAAAAABzguOC44Ljg` 133 | parsedStamp, err := NewServerStampFromString(stamp) 134 | if err != nil { 135 | t.Fatal(err) 136 | } 137 | if parsedStamp.ServerAddrStr != "8.8.8.8:53" { 138 | t.Errorf("expected server address 8.8.8.8 but got %q", parsedStamp.ServerAddrStr) 139 | } 140 | ps := parsedStamp.String() 141 | if ps != stamp { 142 | t.Errorf("re-parsed stamp string is %q, but %q expected", ps, stamp) 143 | } 144 | } 145 | 146 | func TestPlainOldDNSWithPort(t *testing.T) { 147 | // [DNSSEC|No Filter|No Log] + 8.8.8.8:8053 148 | const stamp = `sdns://AAcAAAAAAAAADDguOC44Ljg6ODA1Mw` 149 | parsedStamp, err := NewServerStampFromString(stamp) 150 | if err != nil { 151 | t.Fatal(err) 152 | } 153 | if parsedStamp.ServerAddrStr != "8.8.8.8:8053" { 154 | t.Errorf("expected server address 8.8.8.8 but got %q", parsedStamp.ServerAddrStr) 155 | } 156 | ps := parsedStamp.String() 157 | if ps != stamp { 158 | t.Errorf("re-parsed stamp string is %q, but %q expected", ps, stamp) 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/jedisct1/go-dnsstamps 2 | 3 | go 1.18 4 | --------------------------------------------------------------------------------