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 {
2022-10-29 21:59:50 +00:00
A [ ] * dns . A
2022-10-30 17:52:05 +00:00
AAAA [ ] * dns . AAAA
2022-10-29 13:50:49 +00:00
TXT * dns . TXT
MX [ ] * dns . MX
CNAME [ ] * dns . CNAME
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
A : [ ] * dns . A {
2023-12-13 19:16:04 +00:00
{ A : net . IPv4 ( 137 , 66 , 40 , 11 ) } ,
{ A : net . IPv4 ( 137 , 66 , 40 , 12 ) } ,
2022-10-29 21:59:50 +00:00
} ,
} ,
"ns1.local-ip.sh." : {
A : [ ] * dns . A {
2023-12-13 19:16:04 +00:00
{ A : 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." : {
A : [ ] * dns . A {
2023-12-13 19:16:04 +00:00
{ A : 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." : {
2022-10-29 21:59:50 +00:00
A : [ ] * dns . A {
2023-12-12 23:40:11 +00:00
{ A : net . IPv4 ( 37 , 16 , 23 , 113 ) } ,
2022-10-30 17:52:05 +00:00
} ,
AAAA : [ ] * dns . AAAA {
2023-12-12 23:40:11 +00:00
{ AAAA : net . IP { 0x2a , 0x09 , 0x82 , 0x80 , 0 , 0x01 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0x1C , 0xC1 , 0xC1 } } ,
2022-10-29 21:59:50 +00:00
} ,
2022-10-29 13:37:31 +00:00
TXT : & dns . TXT {
Txt : [ ] string {
"sl-verification=frudknyqpqlpgzbglkqnsmorfcvxrf" ,
"v=spf1 include:simplelogin.co ~all" ,
} ,
2022-10-29 13:25:40 +00:00
} ,
2022-10-29 13:37:31 +00:00
MX : [ ] * dns . MX {
2022-10-29 13:25:40 +00:00
{ Preference : 10 , Mx : "mx1.simplelogin.co." } ,
{ Preference : 20 , Mx : "mx2.simplelogin.co." } ,
} ,
2022-10-29 13:01:07 +00:00
} ,
2022-10-29 14:11:43 +00:00
"_dmarc.local-ip.sh." : {
TXT : & dns . TXT {
Txt : [ ] string { "v=DMARC1; p=quarantine; pct=100; adkim=s; aspf=s" } ,
} ,
} ,
2022-10-29 13:50:49 +00:00
"dkim._domainkey.local-ip.sh." : {
CNAME : [ ] * dns . CNAME {
{ Target : "dkim._domainkey.simplelogin.co." } ,
} ,
} ,
"dkim02._domainkey.local-ip.sh." : {
CNAME : [ ] * dns . CNAME {
{ Target : "dkim02._domainkey.simplelogin.co." } ,
} ,
} ,
"dkim03._domainkey.local-ip.sh." : {
CNAME : [ ] * dns . CNAME {
{ Target : "dkim03._domainkey.simplelogin.co." } ,
} ,
} ,
2022-10-30 17:52:05 +00:00
"_acme-challenge.local-ip.sh." : {
2023-12-13 21:13:35 +00:00
// required for fly.io to obtain a certificate for the website
2023-12-12 23:40:11 +00:00
CNAME : [ ] * dns . CNAME {
{ Target : "local-ip.sh.zzkxm3.flydns.net." } ,
} ,
2023-12-13 21:13:35 +00:00
// will be filled in later when requesting the wildcard certificate
2023-02-26 13:06:15 +00:00
TXT : & dns . TXT { } ,
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 {
records . TXT = & dns . TXT {
Txt : [ ] string { value } ,
}
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 {
records . TXT = nil
hardcodedRecords [ "_acme-challenge.local-ip.sh." ] = records
}
}
2022-10-29 21:59:50 +00:00
func ( xip * Xip ) fqdnToA ( fqdn string ) [ ] * dns . A {
2022-10-29 13:01:07 +00:00
if hardcodedRecords [ strings . ToLower ( fqdn ) ] . A != nil {
2022-10-29 21:59:50 +00:00
var records [ ] * dns . A
for _ , record := range hardcodedRecords [ strings . ToLower ( fqdn ) ] . A {
records = append ( records , & dns . A {
Hdr : dns . RR_Header {
2023-12-13 21:13:35 +00:00
Ttl : uint32 ( ( time . Hour * 24 * 7 ) . Seconds ( ) ) ,
2022-10-29 21:59:50 +00:00
Name : fqdn ,
Rrtype : dns . TypeA ,
Class : dns . ClassINET ,
} ,
A : record . A ,
} )
}
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 {
2023-12-13 21:13:35 +00:00
Ttl : uint32 ( ( time . Hour * 24 * 7 ) . 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
if hardcodedRecords [ strings . ToLower ( fqdn ) ] . AAAA == nil {
2022-11-06 18:29:39 +00:00
xip . answerWithAuthority ( question , message )
2022-10-30 17:52:05 +00:00
return
}
for _ , record := range hardcodedRecords [ strings . ToLower ( fqdn ) ] . AAAA {
message . Answer = append ( message . Answer , & dns . AAAA {
Hdr : dns . RR_Header {
2023-12-13 21:13:35 +00:00
Ttl : uint32 ( ( time . Hour * 24 * 7 ) . Seconds ( ) ) ,
2022-10-30 17:52:05 +00:00
Name : fqdn ,
Rrtype : dns . TypeAAAA ,
Class : dns . ClassINET ,
} ,
AAAA : record . AAAA ,
} )
}
}
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 {
2023-12-13 21:13:35 +00:00
Ttl : uint32 ( ( time . Hour * 24 * 7 ) . 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
}
2022-10-29 13:01:07 +00:00
func ( xip * Xip ) handleTXT ( question dns . Question , message * dns . Msg ) {
fqdn := question . Name
if hardcodedRecords [ strings . ToLower ( fqdn ) ] . TXT == nil {
2022-11-06 18:29:39 +00:00
xip . answerWithAuthority ( question , message )
2022-10-29 13:01:07 +00:00
return
}
message . Answer = append ( message . Answer , & dns . TXT {
Hdr : dns . RR_Header {
2023-12-13 21:13:35 +00:00
Ttl : uint32 ( ( time . Hour * 24 * 7 ) . Seconds ( ) ) ,
2022-10-29 13:01:07 +00:00
Name : fqdn ,
Rrtype : dns . TypeTXT ,
Class : dns . ClassINET ,
} ,
2022-10-29 13:37:31 +00:00
Txt : hardcodedRecords [ strings . ToLower ( fqdn ) ] . TXT . Txt ,
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
if hardcodedRecords [ strings . ToLower ( fqdn ) ] . MX == nil {
2022-11-06 18:29:39 +00:00
xip . answerWithAuthority ( question , message )
2022-10-29 13:25:40 +00:00
return
}
for _ , record := range hardcodedRecords [ strings . ToLower ( fqdn ) ] . MX {
message . Answer = append ( message . Answer , & dns . MX {
Hdr : dns . RR_Header {
2023-12-13 21:13:35 +00:00
Ttl : uint32 ( ( time . Hour * 24 * 7 ) . 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
if hardcodedRecords [ strings . ToLower ( fqdn ) ] . CNAME == nil {
2022-11-06 18:29:39 +00:00
xip . answerWithAuthority ( question , message )
2022-10-29 13:50:49 +00:00
return
}
for _ , record := range hardcodedRecords [ strings . ToLower ( fqdn ) ] . CNAME {
message . Answer = append ( message . Answer , & dns . CNAME {
Hdr : dns . RR_Header {
2023-12-13 21:13:35 +00:00
Ttl : uint32 ( ( time . Hour * 24 * 7 ) . Seconds ( ) ) ,
2022-10-29 13:50:49 +00:00
Name : fqdn ,
Rrtype : dns . TypeCNAME ,
Class : dns . ClassINET ,
} ,
Target : record . 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 ,
Ttl : uint32 ( ( time . Hour * 24 * 7 ) . 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 )
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
}
response . WriteMsg ( message )
} ( )
}
func ( xip * Xip ) StartServer ( ) {
2023-02-26 04:54:58 +00:00
log . Printf ( "Listening on %s\n" , xip . server . Addr )
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 ( ) )
}
}
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
}