Gandi (no) bullshit
Gandi est un acteur bien connu en France pour son activité de "Gestionnaire de nom de domaine" (registrar en Anglais). Enfin, tout du moins par les profils techniques qui cherchent à gérer leurs noms de domaine ! La réputation de la société (fondée en 2000) s'est bâtie sur sa devise "no bullshit" : l'offre commerciale n'est pas la plus avantageuse, mais derrière l'on retrouve des équipes solides techniquement avec un sens éthique développé (de nombreux organismes à but non lucratif sont sponsorisés). Pour se diversifier, ils ont étendus au fil des années leur offre avec des certificats SSL, ainsi que des hébergement virtuels et physiques.
Malheureusement en 2019, Gandi se fait racheter par un fond de capital-investissement (Montefiore Investissements). Il y a une première vague de départs de la part des clients, alors qu'aucune annonce concrète n'est faite (mais parce-que l'on sait tous comment ça va se finir). En 2023, après quatre années stables, nouveau bouleversement avec la fusion entre Gandi et le groupe Néerlandais Total Webhosting Solutions (TWS) pour former Your.Online. Suite à cette fusion, l'ensemble des clients a eu la surprise de découvrir une augmentation générale des tarifs, ainsi que la partie mail va devenir payante (4€ HT/mois/boîte pour l'offre de base). On peut comprendre une augmentation des tarifs vu de l'inflation actuelle (minime pour un .fr (< 0.5€HT/an)). Mais, ne plus avoir ne serait-ce qu'une adresse mail associée à son nom de domaine (qui est une pratique courante dans le milieu) est rédhibitoire pour beaucoup de personnes. Il y a clairement une recherche maximale de rentabilité au détriment des clients et de l'éthique. Tout du moins de la part de la direction, les équipes techniques devant se contenter de suivre.
Pour ma part, je vais encore rester chez Gandi, car leur solution technique tient la route et que je n'utilisais le mail que comme "relai" pour émettre mon courrier (et ne pas tomber dans les filtres anti-spams), gérant moi-même mes serveurs mails. Néanmoins, je dois configurer une nouvelle entrée "PTR" dans le DNS (qui n'est autre qu'un DNS inversé), notamment requis par Gmail. J'en profite donc pour mettre mon script à jour. Si ça ne tient pas sur le long terme, j'utiliserai de nouveau un relai, notamment via Proton Mail qui a l'air fort sympathique.
Le script est disponible ici
#!/usr/bin/env python3
#-*- encoding: utf-8 -*-
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import requests
import json
import re
import socket
import ipaddress
# Config
domain="soutade.fr"
API_KEY = "MY_API_KEY"
livedns_api = "https://dns.api.gandi.net/api/v5/"
dyndns_url = 'http://checkip.dyndns.com/'
headers = {
'X-Api-Key': API_KEY,
}
A_RECORD_NAME="@" # Target record to update
# https://www.programcreek.com/python/?CodeExample=get+local+address
def get_ipv6():
s = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM)
s.connect(('2001:4860:4860::8888', 1))
return s.getsockname()[0]
def get_ipv4():
response = requests.get(dyndns_url)
if response.ok:
pattern = re.compile('[^:]*(\d+\.\d+\.\d+\.\d+)')
result = pattern.search(response.text, 0)
if result == None:
print("No IP found")
exit(1)
else:
return result.group(0).strip()
# Bad gateway
elif response.status_code in (502,504):
exit(0)
else:
print("Connexion error")
response.raise_for_status()
exit(1)
def update_gandi_record(domain_records_href, target, value):
# Get recorded IP
response = requests.get(f'{domain_records_href}/{A_RECORD_NAME}/{target}', headers=headers)
if (response.ok):
record = response.json()
else:
print("Failed to look for recorded IP")
if response.status_code != 502: # Bad gateway
response.raise_for_status()
exit(1)
if value != record['rrset_values'][0]:
record['rrset_values'][0] = value
print(f'URL {domain_records_href}/{A_RECORD_NAME}/{target}')
# PUT new IP
response = requests.put(f'{domain_records_href}/{A_RECORD_NAME}/{target}',
headers=headers, json=record)
if (response.ok):
print("IP updated")
else:
print("something went wrong")
if response.status_code != 502: # Bad gateway
response.raise_for_status()
exit(1)
return 34 # IP updated return !!
return 0
def create_gandi_record(domain_records_href, name, _type, value):
request = {
'rrset_name':name,
'rrset_type': _type,
'rrset_values': [value],
'rrset_ttl': 300
}
response = requests.post(f'{domain_records_href}', headers=headers, json=request)
if response.status_code == 201:
return 0
else:
print(response)
return 1
def delete_gandi_records(domain_records_href, _type):
response = requests.get(f'{domain_records_href}?rrset_type={_type}', headers=headers)
if (response.ok):
json_resp = response.json()
for record in json_resp:
requests.delete(record['rrset_href'], headers=headers)
return 0
else:
print(response)
return 1
# Get current IP
current_ip_v4 = get_ipv4()
print(f'Your Current IP is {current_ip_v4}')
# Retrieve domains address
response = requests.get(livedns_api + "domains", headers=headers)
if (response.ok):
domains = response.json()
else:
if response.status_code != 502: # Bad gateway
response.raise_for_status()
exit(1)
domain_index = next((index for (index, d) in enumerate(domains) if d["fqdn"] == domain), None)
if domain_index == None:
# domain not found
print("The requested domain {domain} was not found in this gandi account")
exit(1)
domain_records_href = domains[domain_index]["domain_records_href"]
ret = update_gandi_record(domain_records_href, 'A', current_ip_v4)
current_ip_v6 = get_ipv6()
print(f'Your Current IP is {current_ip_v6}')
domain_records_href = domains[domain_index]["domain_records_href"]
ret |= update_gandi_record(domain_records_href, 'AAAA', current_ip_v6)
if ret == 34:
# Delete all PTR records
delete_gandi_records(domain_records_href, 'PTR')
# Update PTR v4
reverse_ip = '.'.join(current_ip_v4.split('.')[::-1])
ptr_ip = f'{reverse_ip}.in-addr.arpa'
create_gandi_record(domain_records_href, ptr_ip, 'PTR', f'{domain}.')
# Update PTR v6
full_ip = ipaddress.ip_address(current_ip_v6).exploded
reverse_ip = '.'.join(full_ip.replace(':', '')[::-1])
ptr_ip = f'{reverse_ip}.in-addr.arpa'
create_gandi_record(domain_records_href, ptr_ip, 'PTR', f'{domain}.')
exit(ret)