2022-10-28 21:36:57 +00:00
package xip
import (
2023-12-12 21:25:39 +00:00
"fmt"
2022-10-28 21:36:57 +00:00
"log"
"net"
"os"
"regexp"
"strings"
"time"
"github.com/miekg/dns"
)
type Xip struct {
2023-02-26 04:54:58 +00:00
server dns . Server
2023-12-12 21:25:39 +00:00
nameServers [ ] * dns . NS
2022-10-28 21:36:57 +00:00
}
2022-10-29 13:01:07 +00:00
type HardcodedRecord struct {
2024-07-17 21:44:20 +00:00
A [ ] net . IP // => dns.A
AAAA [ ] net . IP // => dns.AAAA
TXT [ ] string // => dns.TXT
2022-10-29 13:50:49 +00:00
MX [ ] * dns . MX
2024-07-17 21:44:20 +00:00
CNAME [ ] string // => dns.CNAME
2024-07-09 23:09:18 +00:00
SRV * dns . SRV
2022-10-29 13:01:07 +00:00
}
2023-12-13 21:27:51 +00:00
const (
zone = "local-ip.sh."
nameservers = "ns1.local-ip.sh.,ns2.local-ip.sh."
)
2022-10-28 21:36:57 +00:00
var (
2022-10-29 08:10:54 +00:00
flyRegion = os . Getenv ( "FLY_REGION" )
dottedIpV4Regex = regexp . MustCompile ( ` (?:^|(?:[\w\d])+\.)(((25[0-5]|(2[0-4]|1\d|[1-9]|)\d)\.?\b) { 4})($|[.-]) ` )
dashedIpV4Regex = regexp . MustCompile ( ` (?:^|(?:[\w\d])+\.)(((25[0-5]|(2[0-4]|1\d|[1-9]|)\d)\-?\b) { 4})($|[.-]) ` )
2022-10-29 13:01:07 +00:00
hardcodedRecords = map [ string ] HardcodedRecord {
"ns.local-ip.sh." : {
2022-10-29 21:59:50 +00:00
// record holding ip addresses of ns1 and ns2
2024-07-17 21:44:20 +00:00
A : [ ] net . IP {
net . IPv4 ( 137 , 66 , 40 , 11 ) ,
net . IPv4 ( 137 , 66 , 40 , 12 ) ,
2022-10-29 21:59:50 +00:00
} ,
} ,
"ns1.local-ip.sh." : {
2024-07-17 21:44:20 +00:00
A : [ ] net . IP {
net . IPv4 ( 137 , 66 , 40 , 11 ) , // fly.io edge-only ip address, see https://community.fly.io/t/custom-domains-certificate-is-stuck-on-awaiting-configuration/8329
2022-10-29 21:59:50 +00:00
} ,
} ,
"ns2.local-ip.sh." : {
2024-07-17 21:44:20 +00:00
A : [ ] net . IP {
net . IPv4 ( 137 , 66 , 40 , 12 ) , // fly.io edge-only ip address #2
2022-10-29 21:59:50 +00:00
} ,
2022-10-29 13:01:07 +00:00
} ,
"local-ip.sh." : {
2024-07-17 21:44:20 +00:00
A : [ ] net . IP {
net . IPv4 ( 137 , 66 , 40 , 11 ) , // fly.io edge-only ip address
2022-10-29 21:59:50 +00:00
} ,
2024-07-17 21:21:54 +00:00
TXT : [ ] string { "v=spf1 include:capsulecorp.dev ~all" } ,
2022-10-29 13:37:31 +00:00
MX : [ ] * dns . MX {
2024-07-09 23:09:18 +00:00
{ Preference : 10 , Mx : "email.capsulecorp.dev." } ,
2022-10-29 13:25:40 +00:00
} ,
2022-10-29 13:01:07 +00:00
} ,
2024-07-09 23:09:18 +00:00
"autodiscover.local-ip.sh." : {
2024-07-17 21:44:20 +00:00
CNAME : [ ] string {
"email.capsulecorp.dev." ,
2022-10-29 14:11:43 +00:00
} ,
} ,
2024-07-09 23:09:18 +00:00
"_autodiscover._tcp.local-ip.sh." : {
SRV : & dns . SRV {
2024-07-17 21:21:54 +00:00
Priority : 0 ,
Weight : 0 ,
Port : 443 ,
Target : "email.capsulecorp.dev." ,
2022-10-29 13:50:49 +00:00
} ,
} ,
2024-07-09 23:09:18 +00:00
"autoconfig.local-ip.sh." : {
2024-07-17 21:44:20 +00:00
CNAME : [ ] string {
"email.capsulecorp.dev." ,
2022-10-29 13:50:49 +00:00
} ,
} ,
2024-07-09 23:09:18 +00:00
"_dmarc.local-ip.sh." : {
2024-07-17 21:21:54 +00:00
TXT : [ ] string { "v=DMARC1; p=none; rua=mailto:postmaster@local-ip.sh; ruf=mailto:admin@local-ip.sh" } ,
2022-10-29 13:50:49 +00:00
} ,
2024-07-09 23:09:18 +00:00
"dkim._domainkey.local-ip.sh." : {
2024-07-17 21:21:54 +00:00
TXT : [ ] string {
"v=DKIM1;k=rsa;t=s;s=email;p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsMW6NFo34qzKRPbzK41GwbWncB8IDg1i2eA2VWznIVDmTzzsqILaBOGv2xokVpzZm0QRF9wSbeVUmvwEeQ7Z6wkfMjawenDEc3XxsNSvQUVBP6LU/xcm1zsR8wtD8r5J+Jm45pNFaateiM/kb/Eypp2ntdtd8CPsEgCEDpNb62LWdy0yzRdZ/M/fNn51UMN8hVFp4YfZngAt3bQwa6kPtgvTeqEbpNf5xanpDysNJt2S8zfqJMVGvnr8JaJiTv7ZlKMMp94aC5Ndcir1WbMyfmgSnGgemuCTVMWDGPJnXDi+8BQMH1b1hmTpWDiVdVlehyyWx5AfPrsWG9cEuDIfXwIDAQAB" ,
2023-12-12 23:40:11 +00:00
} ,
2024-07-09 23:09:18 +00:00
} ,
"_acme-challenge.local-ip.sh." : {
2023-12-13 21:13:35 +00:00
// will be filled in later when requesting the wildcard certificate
2024-07-17 21:21:54 +00:00
TXT : [ ] string { } ,
2022-10-30 17:52:05 +00:00
} ,
2022-10-29 08:10:54 +00:00
}
2022-10-28 21:36:57 +00:00
)
2023-02-26 10:02:30 +00:00
func ( xip * Xip ) SetTXTRecord ( fqdn string , value string ) {
log . Printf ( "trying to set TXT record \"%s\" for fqdn \"%s\"" , value , fqdn )
if fqdn != "_acme-challenge.local-ip.sh." {
log . Println ( "not allowed, abort" )
return
}
if records , ok := hardcodedRecords [ fqdn ] ; ok {
2024-07-17 21:21:54 +00:00
records . TXT = [ ] string { value }
2023-02-26 10:02:30 +00:00
hardcodedRecords [ "_acme-challenge.local-ip.sh." ] = records
}
}
func ( xip * Xip ) UnsetTXTRecord ( fqdn string ) {
log . Printf ( "trying to unset TXT record for fqdn \"%s\"" , fqdn )
if fqdn != "_acme-challenge.local-ip.sh." {
log . Println ( "not allowed, abort" )
return
}
if records , ok := hardcodedRecords [ fqdn ] ; ok {
2024-07-17 21:21:54 +00:00
records . TXT = [ ] string { }
2023-02-26 10:02:30 +00:00
hardcodedRecords [ "_acme-challenge.local-ip.sh." ] = records
}
}
2022-10-29 21:59:50 +00:00
func ( xip * Xip ) fqdnToA ( fqdn string ) [ ] * dns . A {
2024-07-17 21:21:54 +00:00
normalizedFqdn := strings . ToLower ( fqdn )
if hardcodedRecords [ normalizedFqdn ] . A != nil {
2022-10-29 21:59:50 +00:00
var records [ ] * dns . A
2024-07-17 21:21:54 +00:00
for _ , record := range hardcodedRecords [ normalizedFqdn ] . A {
2022-10-29 21:59:50 +00:00
records = append ( records , & dns . A {
Hdr : dns . RR_Header {
2024-07-17 21:21:54 +00:00
Ttl : uint32 ( ( time . Minute * 5 ) . Seconds ( ) ) ,
2022-10-29 21:59:50 +00:00
Name : fqdn ,
Rrtype : dns . TypeA ,
Class : dns . ClassINET ,
} ,
2024-07-17 21:44:20 +00:00
A : record ,
2022-10-29 21:59:50 +00:00
} )
}
return records
2022-10-29 13:01:07 +00:00
}
2022-10-29 08:10:54 +00:00
2022-10-29 13:01:07 +00:00
for _ , ipV4RE := range [ ] * regexp . Regexp { dashedIpV4Regex , dottedIpV4Regex } {
if ipV4RE . MatchString ( fqdn ) {
match := ipV4RE . FindStringSubmatch ( fqdn ) [ 1 ]
match = strings . ReplaceAll ( match , "-" , "." )
2022-10-29 21:59:50 +00:00
ipV4Address := net . ParseIP ( match ) . To4 ( )
if ipV4Address == nil {
return nil
}
return [ ] * dns . A { {
Hdr : dns . RR_Header {
2024-07-17 21:21:54 +00:00
Ttl : uint32 ( ( time . Minute * 5 ) . Seconds ( ) ) ,
2022-10-29 21:59:50 +00:00
Name : fqdn ,
Rrtype : dns . TypeA ,
Class : dns . ClassINET ,
} ,
A : ipV4Address ,
} }
2022-10-29 08:10:54 +00:00
}
}
2022-10-29 22:41:08 +00:00
return nil
}
2022-11-06 18:29:39 +00:00
func ( xip * Xip ) answerWithAuthority ( question dns . Question , message * dns . Msg ) {
2023-02-26 04:54:58 +00:00
message . Ns = append ( message . Ns , xip . soaRecord ( question ) )
2022-10-29 08:10:54 +00:00
}
2022-10-28 22:27:58 +00:00
func ( xip * Xip ) handleA ( question dns . Question , message * dns . Msg ) {
2022-10-28 21:36:57 +00:00
fqdn := question . Name
2022-10-29 21:59:50 +00:00
records := xip . fqdnToA ( fqdn )
2022-10-28 21:36:57 +00:00
2022-10-29 21:59:50 +00:00
if len ( records ) == 0 {
2022-11-06 18:29:39 +00:00
message . Rcode = dns . RcodeNameError
xip . answerWithAuthority ( question , message )
2022-10-29 08:10:54 +00:00
return
}
2022-10-28 21:36:57 +00:00
2022-10-29 21:59:50 +00:00
for _ , record := range records {
log . Printf ( "(%s) %s => %s\n" , flyRegion , fqdn , record . A )
message . Answer = append ( message . Answer , record )
}
2022-10-29 08:10:54 +00:00
}
2022-10-30 17:52:05 +00:00
func ( xip * Xip ) handleAAAA ( question dns . Question , message * dns . Msg ) {
fqdn := question . Name
2024-07-17 21:21:54 +00:00
normalizedFqdn := strings . ToLower ( fqdn )
if hardcodedRecords [ normalizedFqdn ] . AAAA == nil {
2022-11-06 18:29:39 +00:00
xip . answerWithAuthority ( question , message )
2022-10-30 17:52:05 +00:00
return
}
2024-07-17 21:21:54 +00:00
for _ , record := range hardcodedRecords [ normalizedFqdn ] . AAAA {
2022-10-30 17:52:05 +00:00
message . Answer = append ( message . Answer , & dns . AAAA {
Hdr : dns . RR_Header {
2024-07-17 21:21:54 +00:00
Ttl : uint32 ( ( time . Minute * 5 ) . Seconds ( ) ) ,
2022-10-30 17:52:05 +00:00
Name : fqdn ,
Rrtype : dns . TypeAAAA ,
Class : dns . ClassINET ,
} ,
2024-07-17 21:44:20 +00:00
AAAA : record ,
2022-10-30 17:52:05 +00:00
} )
}
}
2022-10-29 08:10:54 +00:00
func ( xip * Xip ) handleNS ( question dns . Question , message * dns . Msg ) {
fqdn := question . Name
nameServers := [ ] * dns . NS { }
additionals := [ ] * dns . A { }
2023-02-26 04:54:58 +00:00
for _ , ns := range xip . nameServers {
2022-10-29 08:10:54 +00:00
nameServers = append ( nameServers , & dns . NS {
Hdr : dns . RR_Header {
2024-07-17 21:21:54 +00:00
Ttl : uint32 ( ( time . Minute * 5 ) . Seconds ( ) ) ,
2022-10-29 08:10:54 +00:00
Name : fqdn ,
Rrtype : dns . TypeNS ,
Class : dns . ClassINET ,
} ,
Ns : ns . Ns ,
} )
2022-10-29 21:59:50 +00:00
additionals = append ( additionals , xip . fqdnToA ( ns . Ns ) ... )
2022-10-29 08:10:54 +00:00
}
for _ , record := range nameServers {
message . Answer = append ( message . Answer , record )
}
2022-10-29 22:17:39 +00:00
for _ , record := range additionals {
message . Extra = append ( message . Extra , record )
}
2022-10-28 21:36:57 +00:00
}
2024-07-17 21:21:54 +00:00
func chunkBy ( str string , chunkSize int ) ( chunks [ ] string ) {
for chunkSize < len ( str ) {
str , chunks = str [ chunkSize : ] , append ( chunks , str [ 0 : chunkSize ] )
}
return append ( chunks , str )
}
2022-10-29 13:01:07 +00:00
func ( xip * Xip ) handleTXT ( question dns . Question , message * dns . Msg ) {
fqdn := question . Name
2024-07-17 21:21:54 +00:00
normalizedFqdn := strings . ToLower ( fqdn )
if hardcodedRecords [ normalizedFqdn ] . TXT == nil {
2022-11-06 18:29:39 +00:00
xip . answerWithAuthority ( question , message )
2022-10-29 13:01:07 +00:00
return
}
2024-07-17 21:21:54 +00:00
for _ , record := range hardcodedRecords [ normalizedFqdn ] . TXT {
message . Answer = append ( message . Answer , & dns . TXT {
Hdr : dns . RR_Header {
Ttl : uint32 ( ( time . Minute * 5 ) . Seconds ( ) ) ,
Name : fqdn ,
Rrtype : dns . TypeTXT ,
Class : dns . ClassINET ,
} ,
Txt : chunkBy ( record , 255 ) ,
} )
}
2022-10-29 13:01:07 +00:00
}
2022-10-29 13:25:40 +00:00
func ( xip * Xip ) handleMX ( question dns . Question , message * dns . Msg ) {
fqdn := question . Name
2024-07-17 21:21:54 +00:00
normalizedFqdn := strings . ToLower ( fqdn )
if hardcodedRecords [ normalizedFqdn ] . MX == nil {
2022-11-06 18:29:39 +00:00
xip . answerWithAuthority ( question , message )
2022-10-29 13:25:40 +00:00
return
}
2024-07-17 21:21:54 +00:00
for _ , record := range hardcodedRecords [ normalizedFqdn ] . MX {
2022-10-29 13:25:40 +00:00
message . Answer = append ( message . Answer , & dns . MX {
Hdr : dns . RR_Header {
2024-07-17 21:21:54 +00:00
Ttl : uint32 ( ( time . Minute * 5 ) . Seconds ( ) ) ,
2022-10-29 13:25:40 +00:00
Name : fqdn ,
Rrtype : dns . TypeMX ,
Class : dns . ClassINET ,
} ,
Mx : record . Mx ,
Preference : record . Preference ,
} )
}
}
2022-10-29 13:50:49 +00:00
func ( xip * Xip ) handleCNAME ( question dns . Question , message * dns . Msg ) {
fqdn := question . Name
2024-07-17 21:21:54 +00:00
normalizedFqdn := strings . ToLower ( fqdn )
if hardcodedRecords [ normalizedFqdn ] . CNAME == nil {
2022-11-06 18:29:39 +00:00
xip . answerWithAuthority ( question , message )
2022-10-29 13:50:49 +00:00
return
}
2024-07-17 21:21:54 +00:00
for _ , record := range hardcodedRecords [ normalizedFqdn ] . CNAME {
2022-10-29 13:50:49 +00:00
message . Answer = append ( message . Answer , & dns . CNAME {
Hdr : dns . RR_Header {
2024-07-17 21:21:54 +00:00
Ttl : uint32 ( ( time . Minute * 5 ) . Seconds ( ) ) ,
2022-10-29 13:50:49 +00:00
Name : fqdn ,
Rrtype : dns . TypeCNAME ,
Class : dns . ClassINET ,
} ,
2024-07-17 21:44:20 +00:00
Target : record ,
2022-10-29 13:50:49 +00:00
} )
}
}
2024-07-17 21:21:54 +00:00
func ( xip * Xip ) handleSRV ( question dns . Question , message * dns . Msg ) {
fqdn := question . Name
normalizedFqdn := strings . ToLower ( fqdn )
if hardcodedRecords [ normalizedFqdn ] . SRV == nil {
xip . answerWithAuthority ( question , message )
return
}
message . Answer = append ( message . Answer , & dns . SRV {
Hdr : dns . RR_Header {
Ttl : uint32 ( ( time . Minute * 5 ) . Seconds ( ) ) ,
Name : fqdn ,
Rrtype : dns . TypeSRV ,
Class : dns . ClassINET ,
} ,
Priority : hardcodedRecords [ normalizedFqdn ] . SRV . Priority ,
Weight : hardcodedRecords [ normalizedFqdn ] . SRV . Weight ,
Port : hardcodedRecords [ normalizedFqdn ] . SRV . Port ,
Target : hardcodedRecords [ normalizedFqdn ] . SRV . Target ,
} )
}
2022-10-29 22:14:03 +00:00
func ( xip * Xip ) handleSOA ( question dns . Question , message * dns . Msg ) {
2023-02-26 04:54:58 +00:00
message . Answer = append ( message . Answer , xip . soaRecord ( question ) )
2022-10-29 22:14:03 +00:00
}
2023-02-26 04:54:58 +00:00
func ( xip * Xip ) soaRecord ( question dns . Question ) * dns . SOA {
2022-10-28 21:36:57 +00:00
soa := new ( dns . SOA )
soa . Hdr = dns . RR_Header {
2023-12-13 21:13:35 +00:00
Name : question . Name ,
Rrtype : dns . TypeSOA ,
Class : dns . ClassINET ,
2024-07-17 21:21:54 +00:00
Ttl : uint32 ( ( time . Minute * 5 ) . Seconds ( ) ) ,
2022-10-28 21:36:57 +00:00
Rdlength : 0 ,
}
2022-10-29 22:14:03 +00:00
soa . Ns = "ns1.local-ip.sh."
2022-10-29 08:10:54 +00:00
soa . Mbox = "admin.local-ip.sh."
2022-10-28 21:36:57 +00:00
soa . Serial = 2022102800
2023-12-13 21:13:35 +00:00
soa . Refresh = uint32 ( ( time . Minute * 15 ) . Seconds ( ) )
soa . Retry = uint32 ( ( time . Minute * 15 ) . Seconds ( ) )
soa . Expire = uint32 ( ( time . Minute * 30 ) . Seconds ( ) )
soa . Minttl = uint32 ( ( time . Minute * 5 ) . Seconds ( ) )
2022-10-28 22:07:09 +00:00
return soa
}
func ( xip * Xip ) handleQuery ( message * dns . Msg ) {
for _ , question := range message . Question {
switch question . Qtype {
case dns . TypeA :
2022-10-28 22:27:58 +00:00
xip . handleA ( question , message )
2022-10-30 17:52:05 +00:00
case dns . TypeAAAA :
xip . handleAAAA ( question , message )
2022-10-29 08:10:54 +00:00
case dns . TypeNS :
xip . handleNS ( question , message )
2022-10-29 13:01:07 +00:00
case dns . TypeTXT :
xip . handleTXT ( question , message )
2022-10-29 13:25:40 +00:00
case dns . TypeMX :
xip . handleMX ( question , message )
2022-10-29 13:50:49 +00:00
case dns . TypeCNAME :
xip . handleCNAME ( question , message )
2024-07-17 21:21:54 +00:00
case dns . TypeSRV :
xip . handleSRV ( question , message )
2022-10-29 22:14:03 +00:00
case dns . TypeSOA :
xip . handleSOA ( question , message )
2022-10-29 22:41:08 +00:00
default :
xip . handleSOA ( question , message )
2022-10-28 22:07:09 +00:00
}
}
2022-10-28 21:36:57 +00:00
}
func ( xip * Xip ) handleDnsRequest ( response dns . ResponseWriter , request * dns . Msg ) {
go func ( ) {
message := new ( dns . Msg )
message . SetReply ( request )
message . Compress = true
message . Authoritative = true
message . RecursionAvailable = false
switch request . Opcode {
case dns . OpcodeQuery :
xip . handleQuery ( message )
default :
2022-10-28 22:07:09 +00:00
message . MsgHdr . Rcode = dns . RcodeRefused
2022-10-28 21:36:57 +00:00
}
2024-07-17 21:21:54 +00:00
error := response . WriteMsg ( message )
if error != nil {
log . Printf ( "Error writing answer \"%s\": %s\n" , message . String ( ) , error . Error ( ) )
}
2022-10-28 21:36:57 +00:00
} ( )
}
func ( xip * Xip ) StartServer ( ) {
2023-02-26 04:54:58 +00:00
err := xip . server . ListenAndServe ( )
defer xip . server . Shutdown ( )
2022-10-28 21:36:57 +00:00
if err != nil {
2023-12-14 00:04:01 +00:00
if strings . Contains ( err . Error ( ) , "fly-global-services: no such host" ) {
// we're not running on fly, bind to 0.0.0.0 instead
port := strings . Split ( xip . server . Addr , ":" ) [ 1 ]
xip . server = dns . Server {
Addr : fmt . Sprintf ( ":%s" , port ) ,
Net : "udp" ,
}
xip . StartServer ( )
return
}
2022-10-28 21:36:57 +00:00
log . Fatalf ( "Failed to start server: %s\n " , err . Error ( ) )
}
2024-07-09 23:09:18 +00:00
log . Printf ( "Listening on %s\n" , xip . server . Addr )
2022-10-28 21:36:57 +00:00
}
2023-12-13 21:27:51 +00:00
func NewXip ( port int ) ( xip * Xip ) {
2022-10-28 21:36:57 +00:00
xip = & Xip { }
2023-12-13 21:27:51 +00:00
for _ , ns := range strings . Split ( nameservers , "," ) {
2023-02-26 04:54:58 +00:00
xip . nameServers = append ( xip . nameServers , & dns . NS { Ns : ns } )
2022-10-28 21:36:57 +00:00
}
2023-02-26 04:54:58 +00:00
xip . server = dns . Server {
2023-12-12 21:25:39 +00:00
Addr : fmt . Sprintf ( "fly-global-services:%d" , port ) ,
2022-10-28 21:36:57 +00:00
Net : "udp" ,
}
dns . HandleFunc ( zone , xip . handleDnsRequest )
return xip
}