J’utilise une connexion VPN sortante depuis mon pare-feu routeur (pfSense): tous les périphériques qui se connectent dans mon réseau local passent donc par ce VPN de façon indiscriminée (ou presque). Je le fais parce que ça embête un peu les trackers. Ceux qui utilisent la localisation de l’adresse IP m’affichent leur publicité (quand elle n’est pas simplement bloquée) en allemand ou en néerlandais, en fonction du VPN utilisé (il y en a plusieurs). C’est particulièrement utile sur certains sites où le contenu est mélangé à de la publicité, le changement de langue le rend immédiatement visible (je ne parle ni allemand, ni néerlandais).
Certains sites ne fonctionnent pas bien cependant, pour des raisons plus ou moins légitimes. Comme c’était assez rare jusqu’à maintenant, je me débrouillais en bricolant, avec diverses astuces. Cela devient de plus en plus un besoin récurrent et je teste une nouvelle solution: px et un script PAC (les deux combinés ou juste le script PAC selon le cas d’utilisation).
Ce qu’il est possible de faire dans pfSense
Dans pfSense, il est possible de créer des règles de pare-feu pour que certaines connexions ne passent pas par la gateway par défaut (sur laquelle le VPN est configuré), mais ces règles sont assez bas niveau: c’est principalement en fonction de l’IP de destination (après résolution DNS), du port réseau, de l’IP source (l’appareil qui envoie la requête)… Pour une configuration qui dépend de l’URL avant la résolution DNS, c’est plus compliqué et plus fragile, donc peu pratique si on doit y toucher régulièrement.
La solution: squid, un script PAC, px
L’idée est d’installer un proxy sur une machine identifiable par son IP auprès de pfSense. Dans mon cas, j’utilise un Raspberry Pi 4. Le proxy est installé dessus, et pfSense a une règle qui indique que toute connexion qui vient de l’IP de ce Raspberry Pi 4 ne passe pas par le VPN, mais sort directement sur la box Internet.
Pour le proxy, on peut utiliser squid. Je ne vais pas parler davantage de squid car ce n’est pas l’objet de l’article. Le site web propose également un wiki très complet. Je considère qu’on a déjà un proxy opérationnel, cet article s’attache principalement au script PAC et à l’outil px. Le rôle de squid est uniquement de faire rebond sur une machine (le Raspberry Pi 4), ce qui permet de créer une règle très simple de routage sur pfSense (pour sortir par la box Internet directement au lieu de passer par la route par défaut qui est le VPN).
On peut ensuite configurer ce proxy dans son navigateur ou via les variables
d’environnement http_proxy et https_proxy, mais l’inconvénient est que
toutes les connexions vont passer par le proxy, ce qui n’est pas ce qu’on
souhaite.
PAC
est un fichier JavaScript avec une fonction FindProxyForURL en point d’entrée,
qui doit retourner une chaîne “DIRECT” ou “PROXY x.x.x.x:yyyy” selon que la
connexion à utiliser pour l’URL doit être directe ou par proxy.
Exemple d’un tel script:
function FindProxyForURL(url, host) {
// Override proxy for a selection of URLs (to bypass router's proxy)
if (shExpMatch(host, "*.boursobank.com") || host == "boursobank.com")
return "PROXY 192.168.0.10:3128";
// DIRECT = no client-side proxy
return "DIRECT";
}
Dans l’exemple ci-dessus, toute requête à https://boursobank.com ou un de ses
sous-domaines passera par le proxy 192.168.0.10:3128. Évidemment, cela suppose
d’avoir un proxy prêt à prendre en charge les requêtes à cette adresse.
Firefox permet d’utiliser un tel script dans sa configuration (actuellement dans Vie privée et sécurité, Sécurité logicielle et des connexions, Configurer le proxy, Adresse de configuration automatique du proxy). Il faut cependant le servir en HTTP/S, il faut donc un serveur local pour cela. Plus bas dans l’article, on préférera modifier les paramètres proxy de Windows et laisser le navigateur utiliser ces derniers, ça évitera de devoir configurer plusieurs navigateurs, voire d’autres applications qui s’appuient sur les paramètres réseau de Windows le cas échéant.
Noter qu’il est théoriquement possible de filtrer par URL (c’est bien l’argument passé à la fonction), mais que son respect va dépendre du cas d’utilisation: Firefox et je suppose la plupart des navigateurs retiennent la décision par domaine (et probablement par port). À moins de tester plusieurs URL sous le même domaine dans des sessions de navigation privées indépendantes, le résultat obtenu ne sera probablement pas le bon: le navigateur n’évalue pas strictement le script pour chaque URL (en tout cas Firefox).
Voici un diagramme Mermaid qui récapitule le dispositif:
flowchart TD AppPac["Navigateur ou application avec script PAC"] AppEnv["Application avec http_proxy / https_proxy"] AppWin["Application avec paramètres proxy Windows"] Px["px local<br>127.0.0.1:3128"] Pac["Script PAC<br>FindProxyForURL"] Squid["squid<br>Raspberry Pi 4<br>192.168.0.10:3128"] PfSense["pfSense"] Vpn["VPN<br>route par defaut"] Box["Box Internet<br>sortie directe"] Internet["Internet"] AppPac --> Pac AppEnv --> Px AppWin --> Px Px --> Pac Pac -->|DIRECT| PfSense Pac -->|PROXY 192.168.0.10:3128| Squid Squid --> PfSense PfSense -->|Client standard| Vpn Vpn --> Internet PfSense -->|IP source = Raspberry Pi| Box Box --> Internet
Servir un script PAC avec un serveur nginx
Voici des exemples de manifestes Kubernetes pour un déploiement nginx et le script PAC sous la forme d’un ConfigMap.
Cette partie est un exemple d’hébergement, n’importe quel serveur HTTP peut convenir.
apiVersion: v1
kind: ConfigMap
metadata:
name: nginx-static-pac
data:
proxy.pac: |
function FindProxyForURL(url, host) {
// Override proxy for a selection of URLs (to bypass router's proxy)
if (shExpMatch(host, "*.boursobank.com") || host == "boursobank.com")
return "PROXY 192.168.0.10:3128";
// DIRECT = no client-side proxy
return "DIRECT";
}
---
apiVersion: v1
kind: ConfigMap
metadata:
name: nginx-static-config
data:
default.conf: |
server {
listen 80;
server_name _;
location = /proxy.pac {
alias /usr/share/nginx/html/proxy.pac;
default_type application/x-ns-proxy-autoconfig;
add_header Cache-Control "max-age=300, must-revalidate";
}
}
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-static
spec:
replicas: 1
selector:
matchLabels:
app: nginx-static
template:
metadata:
labels:
app: nginx-static
spec:
containers:
- name: nginx
image: nginx:1.31.2-alpine-slim
ports:
- containerPort: 80
name: http
volumeMounts:
- name: pac-files
mountPath: /usr/share/nginx/html
- name: nginx-config
mountPath: /etc/nginx/conf.d
volumes:
- name: pac-files
configMap:
name: nginx-static-pac
- name: nginx-config
configMap:
name: nginx-static-config
---
apiVersion: v1
kind: Service
metadata:
name: nginx-static-svc
spec:
selector:
app: nginx-static
ports:
- name: http
port: 80
targetPort: http
---
apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata:
name: nginx-static-ingress-https
spec:
entryPoints:
- websecure
routes:
- match: Host(`static.my-domain.com`)
kind: Rule
services:
- name: nginx-static-svc
port: 80
scheme: http
tls:
certResolver: acme
Une fois déployé, notre script PAC est disponible en téléchargement sur l’URL
https://static.my-domain.com/proxy.pac. J’utilise HTTPS avec ma configuration
Traefik et Let’s Encrypt mais une URL en HTTP devrait pouvoir fonctionner aussi
bien pour servir ce script PAC.
Maintenant, si on configure cette URL dans Firefox, on devrait voir qu’il est utilisé. Pour le tester, on peut ajouter des sites témoins au script PAC, par exemple deux sites qui permettent d’obtenir notre adresse IP publique, l’un avec une connexion directe, l’autre avec le proxy. En testant dans Firefox, on devrait voir respectivement l’adresse IP du VPN et notre adresse IP publique de box internet. La moindre erreur dans le script JavaScript rendra la configuration inopérante (= connexion directe en fallback).
On peut également configurer le script PAC non pas dans chaque navigateur, mais dans les paramètres réseau et Internet de Windows: Paramètres, Réseau et Internet, Proxy, Utiliser un script de configuration. Le script sera utilisé par toutes les applications qui utilisent les paramètres proxy de Windows.
En revanche, certaines applications ignorent les paramètres proxy de Windows, et
ne permettent pas non plus de configurer un script PAC. En dernier recours, on
peut espérer qu’elles supportent les
variables d’environnement http_proxy et https_proxy
qui sont devenues une forme de standard de facto.
px
Comme on ne peut pas mettre un script PAC directement dans les variables
d’environnement http_proxy et https_proxy, l’alternative est de faire
tourner un (autre) proxy de rebond local qui supporte les scripts PAC. C’est le
cas de px.
Un autre avantage intéressant de px est qu’il supporte un fichier PAC local, donc le serveur nginx peut être évité. Dans mon cas, je préfère malgré tout le conserver, car il reste utile pour centraliser la configuration des proxies sur plusieurs périphériques, dont l’iPhone.
Pour l’installation, voir la documentation.
Dans mon cas, je l’ai installé via winget install genotrance.px.
Une fois installé, on peut créer un fichier de configuration “px.ini” (le nom importe peu, car on passera son chemin en ligne de commande):
[proxy]
pac = https://static.my-domain.com/proxy.pac
port = 3128
listen = 127.0.0.1
gateway = 0
hostonly = 1
noproxy = localhost,127.0.0.1
[settings]
foreground = 0
log = 4
Si le proxy squid requiert une authentification, on peut ajouter les paramètres
username = <USER> et auth = BASIC sous la section [proxy]. Attention, il
faudra sauver le mot de passe dans le gestionnaire de Windows en lançant une
première fois la commande de px avec l’option supplémentaire --password pour
avoir un prompt dans lequel saisir le mot de passe. J’ai fini par supprimer
l’authentification à squid car ce n’est pas géré par toutes les applications. En
gros, c’est principalement bien géré par les navigateurs et par tout ce qui
passe par px. En revanche, sur iPhone notamment, si on configure le script PAC
comme décrit plus bas, l’authentification pourra gêner le fonctionnement de
certaines applications.
Le paramètre settings.log = 4 sert à afficher les logs dans la sortie console
de px, ce qui est bien utile pour confirmer le bon fonctionnement, mais on
pourra ensuite mettre la valeur 0 pour les désactiver.
Pour le lancer (adapter le chemin complet vers le fichier px.ini créé précédemment):
px --config=px.ini
Dans les paramètres proxy de Windows, on peut remplacer la configuration PAC par
un proxy simple: localhost, port 3128. Ne pas oublier de mettre dans le
champ texte qui gère les exclusions le domaine qui sert le script PAC pour
éviter une boucle (dans mon exemple: static.my-domain.com). On pourra aussi
ajouter tout ce qu’on mettra dans la variable no_proxy (voir plus bas).
Peu de temps après, on doit voir pas mal de logs s’afficher dans la sortie console de px.
Lancement de px à l’ouverture de session Windows
Pour démarrer px automatiquement avec Windows:
px --config=px.ini --install
Cela crée une clé dans le registre Windows à l’emplacement
HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Run: à l’ouverture
de session Windows, la commande px --config=px.ini sera lancée.
Pour retirer cette clé, utiliser l’option --uninstall.
Noter que l’installation ne fait que créer la clé, mais ne lance pas px. Il faut donc le relancer si on ne souhaite pas rouvrir sa session pour déclencher ce lancement.
La commande indiquée va ouvrir une fenêtre console. Si on préfère le mode
arrière-plan, il faut remplacer px par pxw. Si on veut stopper pxw qui
tourne en arrière-plan, la commande est pxw --quit. Le mode arrière-plan est
celui à privilégier au jour le jour, mais pour des tests, il peut être utile de
suivre la sortie console en lançant l’outil en premier-plan.
Variables http_proxy, https_proxy et no_proxy
Maintenant que px est opérationnel, on peut définir ces trois variables d’environnement de manière globale pour l’utilisateur avec ces valeurs:
http_proxy=http://localhost:3128
https_proxy=http://localhost:3128
no_proxy=localhost,127.0.0.1,host.docker.internal,gateway.docker.internal,kubernetes.docker.internal
Ainsi, toutes les applications qui respectent ces variables passeront également
par px quand le domaine (ou le sous-domaine) ne fait pas partie des domaines
listés dans no_proxy.
Cela n’est jamais une garantie, mais la plupart des outils en ligne de commande
respectent cette convention. Pour tester le bon fonctionnement, il y a les logs
côté px (via sa sortie console si on le lance manuellement), les logs côté squid
(sudo tail -f /var/log/squid/access.log), ou éventuellement le test de
résolution de son adresse IP publique si elle diffère selon le chemin de la
requête (ce qui est le cas pour un VPN sortant).
Ces variables ne sont pas un standard bien défini, donc il peut être utile de
prendre connaissance de l’article mentionné plus haut:
We need to talk: Can we standardize NO_PROXY?
car il y a des choses à savoir. Très succinctement, préférer la version
minuscules à la version majuscules (http_proxy et non HTTP_PROXY), des
domaines simples dans no_proxy (éviter les wildcard sauf pour des tests
spécifiques car leur interprétation varie).
Pour la valeur de no_proxy, je me suis basé sur mon fichier hosts sur
Windows. Dans cet article j’ai laissé les domaines liés à Docker car assez
répandus sur un poste de développeur, mais j’ai omis certains domaines plus
spécifiques à mon installation, que j’utilise bien dans cette variable. En cas
de mise à jour du fichier hosts sur le poste sur lequel px tourne, il peut être
préférable d’adapter la variable no_proxy en conséquence.
Configurer le script PAC sur iPhone
Sur iPhone, on peut configurer l’URL du script PAC dans les réglages du réseau Wi-Fi.
Je suppose que sur Android, il existe une configuration équivalente, mais je n’ai pas testé.
Prise en compte des changements dans le script
L’exemple plus haut dans la configuration nginx indique aux clients un délai d’expiration de 5 minutes au-delà duquel ils doivent revalider le script.
Je ne sais pas dans quelle mesure ce délai est respecté par les différents
clients (il me semble probable que cela varie d’un système à l’autre), mais dans
le pire des cas, si la prise en compte rapide est très importante, il devrait
suffire soit de redémarrer la machine (ou l’iPhone), soit de modifier l’URL du
script en ajoutant une version en paramètre d’URL, par exemple ?v=2 pour
invalider la précédente version.
Sur Windows, si on définit les paramètres proxy sur px
(http://localhost:3128), il suffit probablement de relancer px pour forcer un
rafraichissement du script PAC. Comme tout passe par px (que ce soit via les
paramètres proxy de Windows ou via les variables d’environnement http_proxy),
c’est pratique à gérer. Si on préfère configurer le script PAC dans les
paramètres de Windows (pour éviter le rebond inutile par px), je suppose qu’on
peut modifier légèrement l’URL du script comme indiqué avant.
La longueur de l’article peut laisser penser que c’est un peu laborieux, mais
c’est en fait relativement simple et pratique à configurer une fois en place.
Évidemment, en cas de défaillance du serveur nginx ou de px pour les clients qui
passent par lui, il faudra se rappeler la présence de cette couche d’indirection
(ça n’apparaîtra pas dans le message d’erreur). J’ai déjà eu besoin de
redémarrer px car il était parti dans les choux: pxw --quit, puis
pxw --config=px.ini dans un terminal et c’est reparti (en adaptant le chemin
vers le fichier de configuration).