[WiFiConnect] Extend captive portal with a URL callback / redirect after successfully connected to the internet

I am trying to customise WiFiConnect (balena block module) - multi container usage .

After digging to the repo I found that there’s a script which uses docker to make the rust build (wificonnect binary).

Below you can see the steps that I proceeded:

user@ubuntu:~/my-iot-project$ cd wifi-connect/
user@ubuntu:~/my-iot-project/wifi-connect$ ./scripts/local-build.sh arm-unknown-linux-gnueabihf rpi

if [ -z "$1" ]; then
        printf 'Rust compilation target not specified'
        exit 1


cross() {
    docker run -it --rm -v $PWD:/work majorz/rust-$ARCH:rust-1.23.0 "$@"

cross cargo build --release --target=$TARGET
    Updating registry `https://github.com/rust-lang/crates.io-index`
 Downloading env_logger v0.4.3                                                  
 Downloading router v0.6.0                                                      
 Downloading iron v0.6.0                                                        
 Downloading clap v2.31.2                                                       
 Downloading log v0.3.9                                                         
 Downloading network-manager v0.11.0                                            
 Downloading staticfile v0.5.0                                                  
 Downloading params v0.8.0                                                      
 Downloading serde_derive v1.0.66                                               
 Downloading nix v0.10.0                                                        
 Downloading persistent v0.4.0                                                  
 Downloading mount v0.4.0                                                       
 Downloading iron-cors v0.7.1                                                   
 Downloading serde v1.0.66                                                      
 Downloading serde_json v1.0.22                                                 
 Downloading error-chain v0.11.0                                                
 Downloading regex v0.2.11                                                      
 Downloading log v0.4.2                                                         
 Downloading cfg-if v0.1.3                                                      
 Downloading memchr v2.0.1                                                      
 Downloading regex-syntax v0.5.6                                                
 Downloading aho-corasick v0.6.4                                                
 Downloading utf8-ranges v1.0.0                                                 
 Downloading thread_local v0.3.5                                                
 Downloading libc v0.2.42                                                       
 Downloading ucd-util v0.1.1                                                    
 Downloading unreachable v1.0.0                                                 
 Downloading lazy_static v1.0.1                                                 
 Downloading void v1.0.2                                                        
 Downloading route-recognizer v0.1.12                                           
 Downloading url v1.7.0                                                         
 Downloading hyper v0.10.13                                                     
 Downloading num_cpus v1.8.0                                                    
 Downloading mime_guess v1.8.4                                                  
 Downloading plugin v0.2.6                                                      
 Downloading modifier v0.1.0                                                    
 Downloading typemap v0.3.3                                                     
 Downloading time v0.1.40                                                       
 Downloading httparse v1.3.1                                                    
 Downloading unicase v1.4.2                                                     
 Downloading base64 v0.6.0                                                      
 Downloading traitobject v0.1.0                                                 
 Downloading language-tags v0.2.2                                               
 Downloading typeable v0.1.2                                                    
 Downloading mime v0.2.6                                                        
 Downloading version_check v0.1.3                                               
 Downloading safemem v0.2.0                                                     
 Downloading byteorder v1.2.3                                                   
 Downloading idna v0.1.4                                                        
 Downloading percent-encoding v1.0.1                                            
 Downloading matches v0.1.6                                                     
 Downloading unicode-bidi v0.3.4                                                
 Downloading unicode-normalization v0.1.7                                       
 Downloading phf v0.7.22                                                        
 Downloading phf_shared v0.7.22                                                 
 Downloading siphasher v0.2.2                                                   
 Downloading phf_codegen v0.7.22                                                
 Downloading phf_generator v0.7.22                                              
 Downloading rand v0.4.2                                                        
 Downloading unsafe-any v0.4.2                                                  
 Downloading textwrap v0.9.0                                                    
 Downloading atty v0.2.10                                                       
 Downloading unicode-width v0.1.5                                               
 Downloading vec_map v0.8.1                                                     
 Downloading strsim v0.7.0                                                      
 Downloading bitflags v1.0.3                                                    
 Downloading ascii v0.8.7                                                       
 Downloading futures-cpupool v0.1.8                                             
 Downloading futures v0.1.21                                                    
 Downloading dbus v0.5.4                                                        
 Downloading tokio-timer v0.1.2                                                 
 Downloading libdbus-sys v0.1.3                                                 
 Downloading pkg-config v0.3.11                                                 
 Downloading slab v0.3.0                                                        
 Downloading sequence_trie v0.3.5                                               
 Downloading multipart v0.13.6                                                  
 Downloading urlencoded v0.6.0                                                  
 Downloading num v0.1.42                                                        
 Downloading bodyparser v0.8.0                                                  
 Downloading tempdir v0.3.7                                                     
 Downloading twoway v0.1.8                                                      
 Downloading rand v0.3.22                                                       
 Downloading buf_redux v0.6.3                                                   
 Downloading remove_dir_all v0.5.1                                              
 Downloading memchr v1.0.2                                                      
 Downloading itoa v0.4.1                                                        
 Downloading dtoa v0.4.2                                                        
 Downloading num-complex v0.1.43                                                
 Downloading num-integer v0.1.39                                                
 Downloading num-rational v0.1.42                                               
 Downloading num-iter v0.1.37                                                   
 Downloading num-bigint v0.1.44                                                 
 Downloading num-traits v0.2.5                                                  
 Downloading rustc-serialize v0.3.24                                            
 Downloading syn v0.14.2                                                        
 Downloading quote v0.6.3                                                       
 Downloading proc-macro2 v0.4.6                                                 
 Downloading unicode-xid v0.1.0                                                 
 Downloading bytes v0.4.8                                                       
 Downloading iovec v0.1.2                                                       
 Downloading ansi_term v0.11.0                                                  
    Finished release [optimized] target(s) in 0.0 secs                          

cross cross-strip target/$TARGET/release/wifi-connect
user@ubuntu:~/my-iot-project/wifi-connect$ cd ..
user@ubuntu:~/my-iot-project$ balena build --deviceType raspberry-pi  --arch rpi --emulated
[Build]   Building services...
[Build]   wifi-connect Preparing...
[Build]   google-iot   Preparing...
[Build]   node-red     Preparing...
[Info]    Building for rpi/raspberry-pi
[Info]    Emulation is enabled
[Build]   google-iot   Step 1/13 : FROM balenalib/raspberry-pi-alpine-node:11-build as build
[Build]   google-iot    ---> 1ac9ba9ead17
[Build]   google-iot   Step 2/13 : COPY [".balena/qemu-execve","/tmp/qemu-execve"]
[Build]   google-iot    ---> Using cache
[Build]   google-iot    ---> 1e4ba92f32c1
[Build]   google-iot   Step 3/13 : COPY ["package*.json","./"]
[Build]   google-iot    ---> Using cache
[Build]   google-iot    ---> a6c0485a3728
[Build]   google-iot   Step 4/13 : RUN ["/tmp/qemu-execve","-execve","/bin/sh","-c","npm install --production && npm cache clean --force && rm -rf /tmp/*"]
[Build]   google-iot    ---> Using cache
[Build]   google-iot    ---> 56b47ae29d36
[Build]   google-iot   Step 5/13 : FROM balenalib/raspberry-pi-alpine-node:11-run as run
[Build]   google-iot    ---> 4c4c34b8d874
[Build]   google-iot   Step 6/13 : COPY [".balena/qemu-execve","/tmp/qemu-execve"]
[Build]   google-iot    ---> Using cache
[Build]   google-iot    ---> d96f02e38d22
[Build]   google-iot   Step 7/13 : VOLUME ["/data"]
[Build]   google-iot    ---> Using cache
[Build]   google-iot    ---> 7c7a4d5b2bc6
[Build]   google-iot   Step 8/13 : RUN ["/tmp/qemu-execve","-execve","/bin/sh","-c","install_packages openssl"]
[Build]   google-iot    ---> Using cache
[Build]   google-iot    ---> be65246c129d
[Build]   google-iot   Step 9/13 : WORKDIR /app
[Build]   google-iot    ---> Using cache
[Build]   google-iot    ---> f1015f99310a
[Build]   google-iot   Step 10/13 : COPY --from=build ["./node_modules","./node_modules"]
[Build]   google-iot    ---> Using cache
[Build]   google-iot    ---> 66175fdb1972
[Build]   google-iot   Step 11/13 : COPY [".","."]
[Build]   google-iot    ---> Using cache
[Build]   google-iot    ---> 282c89190aac
[Build]   google-iot   Step 12/13 : RUN ["/tmp/qemu-execve","-execve","/bin/sh","-c","chmod +x ./wait-for-it/wait-for-it.sh"]
[Build]   google-iot    ---> Using cache
[Build]   google-iot    ---> f57a716a0f28
[Build]   google-iot   Step 13/13 : CMD ["npm","start","sh -c","/app/wait-for-it/wait-for-it.sh www.google.com:80 -- echo My IoT device is connected to the internet!"]
[Build]   google-iot    ---> Using cache
[Build]   google-iot    ---> c672bf69c123
[Build]   google-iot   Successfully built c672bf69c123
[Build]   google-iot   Successfully tagged my-iot-project_google-iot:latest
[Build]   node-red     Step 1/14 : FROM balenalib/raspberry-pi-debian-node:12-stretch-build as build
[Build]   node-red      ---> 0b0610014d32
[Build]   node-red     Step 2/14 : COPY [".balena/qemu-execve","/tmp/qemu-execve"]
[Build]   node-red      ---> Using cache
[Build]   node-red      ---> de4109795fe0
[Build]   node-red     Step 3/14 : RUN ["/tmp/qemu-execve","-execve","/bin/sh","-c","JOBS=MAX npm install -g --production --unsafe-perm        node-red        node-red-admin        node-red-contrib-balena        node-red-node-sqlite        node-red-contrib-modbus        node-red-dashboard        node-red-contrib-google-iot-core"]
[Build]   node-red      ---> Using cache
[Build]   node-red      ---> 773df59ee154
[Build]   node-red     Step 4/14 : FROM balenalib/raspberry-pi-debian-node:12-stretch-run
[Build]   node-red      ---> d08fcf28568b
[Build]   node-red     Step 5/14 : COPY [".balena/qemu-execve","/tmp/qemu-execve"]
[Build]   node-red      ---> Using cache
[Build]   node-red      ---> 0a56ca994b5e
[Build]   node-red     Step 6/14 : RUN ["/tmp/qemu-execve","-execve","/bin/sh","-c","apt-get update && apt-get install -yq --no-install-recommends   rpi.gpio   python-dev   python-rpi.gpio   libatomic1   && apt-get clean && rm -rf /var/lib/apt/lists/*"]
[Build]   node-red      ---> Using cache
[Build]   node-red      ---> d6c68ddcf5cd
[Build]   node-red     Step 7/14 : WORKDIR /usr/src/app
[Build]   node-red      ---> Using cache
[Build]   node-red      ---> 867ac4c7858e
[Build]   node-red     Step 8/14 : COPY --from=build ["/usr/local/bin","/usr/local/bin"]
[Build]   node-red      ---> Using cache
[Build]   node-red      ---> 355356a75947
[Build]   node-red     Step 9/14 : COPY --from=build ["/usr/local/lib/node_modules","/usr/local/lib/node_modules"]
[Build]   node-red      ---> Using cache
[Build]   node-red      ---> 6ecd9e5ac2d5
[Build]   node-red     Step 10/14 : COPY ["./app","./"]
[Build]   node-red      ---> Using cache
[Build]   node-red      ---> a2642a2aa830
[Build]   node-red     Step 11/14 : COPY ["./data","./data"]
[Build]   node-red      ---> Using cache
[Build]   node-red      ---> 278cfeeec540
[Build]   node-red     Step 12/14 : COPY ["./wait-for-it","./wait-for-it"]
[Build]   node-red      ---> Using cache
[Build]   node-red      ---> a248c4e45c96
[Build]   node-red     Step 13/14 : RUN ["/tmp/qemu-execve","-execve","/bin/sh","-c","chmod +x ./wait-for-it/wait-for-it.sh"]
[Build]   node-red      ---> Using cache
[Build]   node-red      ---> f5f4afa8baa3
[Build]   node-red     Step 14/14 : CMD ["bash","/usr/src/app/start.sh","bash","/usr/src/app/wait-for-it/wait-for-it.sh google-iot:8883 -- echo Google IoT Service is up and running"]
[Build]   node-red      ---> Using cache
[Build]   node-red      ---> efeeeef68dd4
[Build]   node-red     Successfully built efeeeef68dd4
[Build]   node-red     Successfully tagged my-iot-project_node-red:latest
[Build]   wifi-connect Step 1/7 : FROM balenablocks/wifi-connect:rpi
[Build]   wifi-connect  ---> e94cd084778c
[Build]   wifi-connect Step 2/7 : COPY [".balena/qemu-execve","/tmp/qemu-execve"]
[Build]   wifi-connect  ---> Using cache
[Build]   wifi-connect  ---> 52614d5cda90
[Build]   wifi-connect Step 3/7 : WORKDIR /usr/src/app
[Build]   wifi-connect  ---> Using cache
[Build]   wifi-connect  ---> f6de08ca7e34
[Build]   wifi-connect Step 4/7 : COPY ["ui","/usr/src/app/ui"]
[Build]   wifi-connect  ---> Using cache
[Build]   wifi-connect  ---> be70db8b2c7b
[Build]   wifi-connect Step 5/7 : COPY ["scripts/start.sh","."]
[Build]   wifi-connect  ---> Using cache
[Build]   wifi-connect  ---> a6db4bbce011
[Build]   wifi-connect Step 6/7 : COPY ["target/arm-unknown-linux-gnueabihf/release/wifi-connect","."]
[Build]   wifi-connect  ---> Using cache
[Build]   wifi-connect  ---> 31775071cae2
[Build]   wifi-connect Step 7/7 : CMD ["/bin/busybox","sh","start.sh"]
[Build]   wifi-connect  ---> Using cache
[Build]   wifi-connect  ---> 5021cce340ad
[Build]   wifi-connect Successfully built 5021cce340ad
[Build]   wifi-connect Successfully tagged my-iot-project_wifi-connect:latest
[Build]   wifi-connect Image size: 26.10 MB
[Build]   google-iot   Image size: 181.08 MB
[Build]   node-red     Image size: 525.91 MB
[Build]   Built 3 services in 0:15
[Success] Build succeeded!
user@ubuntu:~/my-iot-project$ balena deploy myIoTProject --source ./
[Info]    Everything is up to date (use --build to force a rebuild)
[Info]    Creating release...
[Info]    Pushing images to registry...
[Info]    Saving release...
[Success] Deploy succeeded!
[Success] Release: 9aac35335f1f9b6ef4d8f6388cf22463

                            _.-(6'  \
                           (=___._/` \
                                )  \ |
                               /   / |
                              /    > /
                             j    < _\
                         _.-' :      ``.
                         \ r=._\        `.
                        <`\\_  \         .`-.
                         \ r-7  `-. ._  ' .  `\
                          \`,      `-.`7  7)   )
                           \/         \|  \'  / `-._
                                      ||    .'
                                       \\  (
                                        >\  >
                                    ,.-' >.'

user@ubuntu:~/my-iot-project$ DEBUG=1 balena preload ./image.img --app myIoTProject --splash-image logo.png --commit latest
[debug] original argv0="balena" argv=[/home/user/balena-cli/balena,/snapshot/versioned-source/bin/balena,preload,./image.img,--app,myIoTProject,--splash-image,logo.png,--commit,latest] length=10
[debug] new argv=[/home/user/balena-cli/balena,/snapshot/versioned-source/bin/balena,preload,./image.img,--app,myIoTProject,--splash-image,logo.png,--commit,latest] length=10
[debug] Event tracking error: Response code 404 (Not Found)
[Warn] Renaming notice: The '-a', '--app' or '--application' options are now
[Warn] aliases for the '-f' or '--fleet' options. THE ALIASES WILL BE REMOVED
[Warn] in the next major version of the balena CLI (so that a different '--app'
[Warn] option can be implemented in the future). Use '-f' or '--fleet' instead.
[Warn] Find out more at: https://git.io/JRuZr
Building Docker preloader image. [===                     ] 12%
Step 1/7 : FROM alpine:3.12
Building Docker preloader image. [======                  ] 25%
Step 2/7 : WORKDIR /usr/src/app
 ---> Using cache
Building Docker preloader image. [=========               ] 37%
Step 3/7 : RUN apk add --no-cache curl py3-pip parted btrfs-progs util-linux sfdisk file coreutils sgdisk e2fsprogs-extra docker
 ---> Using cache
Building Docker preloader image. [============            ] 50%
Step 4/7 : COPY requirements.txt ./
 ---> Using cache
Building Docker preloader image. [===============         ] 62%
Step 5/7 : RUN pip3 install --no-cache-dir -r requirements.txt
 ---> Using cache
Building Docker preloader image. [==================      ] 75%
Step 6/7 : COPY src/ ./
 ---> Using cache
Building Docker preloader image. [=====================   ] 87%
Step 7/7 : CMD ["python3", "/usr/src/app/preload.py"]
 ---> Using cache
 ---> 896419830cbb
Successfully built 896419830cbb
Building Docker preloader image. [========================] 100%
| Checking that the image is a writable file
| Finding a free tcp port
| Checking if the image is an edison zip archive
| Creating preloader container
/ Starting preloader container
/ Fetching application admin/myIoTProject
| Reading image informationWaiting for Docker to start...
\ Reading image informationDocker started
- Reading image information
- Fetching application 21
- Estimating required additional space
/ Resizing partitions and waiting for dockerd to startReplacing splash image
- Resizing partitions and waiting for dockerd to startExpanding extended partition n°4 of /img/balena.img
Expanding logical partition n°6 of /img/balena.img
\ Resizing partitions and waiting for dockerd to startResizing ext4 filesystem of partition n°6 of /img/balena.img using /dev/loop15
| Resizing partitions and waiting for dockerd to startFile system OK
| Resizing partitions and waiting for dockerd to startWaiting for Docker to start...
/ Resizing partitions and waiting for dockerd to startDocker started

Pulling 3 images [========================] 100%
/ Cleaning up temporary files

After I preloaded the app to my clean image v2.83.21+rev1.dev.img, here are some pictures with the logs:

I am trying to extend the captive portal functionality to redirect the user to a specific url.

I have tried to do it on this file, using the following code:

let url = Url::parse(&format!("mycustomurl://aaa-{}", &env::var("A_VAR_ENV").unwrap_or("aaa-123".to_string()))).unwrap();
return Ok(Response::with((status::Found, Redirect(url))));

but after connecting to the portal and setting the password the captive portal keeps restarting and I am not redirected to my custom URL.

Another try was to modify, this file following the next steps:

  1. Adding the env variable to the header response
  2. Modifying the react app to read the header and redirect the browser to the specific url.

none of them proved to be working. Any idea how to solve this?

1 Like

Thank you for sharing all of the steps @elsevero

my idea was to modify wifi-connect/network.rs at master · balena-os/wifi-connect · GitHub

What’s the problem that you got in this case? is the container restarting and checking for Internet? Did you try to avoid this loop of checking Internet connection with a variable?

Hi @mpous ,

Thank you for your prompt reply!

Ok. Will focus on that area.

Currently the problem that I am facing is that:

  1. I am connecting to the Captive portal
  2. I am setting the wifi password of my local network
  3. Gets connected and if I ping to google.com it returns that the internet connection is successfully.
  4. After a short time it gets disconnected from the internet and the WiFi Connect switches to Access Point mode.
  5. I need to do all the steps all over again.

And to answer to your questions:

What’s the problem that you got in this case?

The problem is that it keeps dropping the internet connection for some reason.

is the container restarting and checking for Internet?

Yes, the container (of wificonnect) keeps restarting.

Did you try to avoid this loop of checking Internet connection with a variable?

Not sure how to do that but if you can share with me some code snippets that would be helpful.

Thank you!

did you check the initial part of the wifi-connect?

Do you have Internet connectivity?

Hi @mpous ,

Yes I have the internet connectivity but only for a period of time.

Please note that I am extracting the docker mention files:

docker-compose.yml (contents):

version: '2.1'

    build: ./wifi-connect
    restart: always
    network_mode: host
    privileged: true
      io.balena.features.dbus: '1'
      io.balena.features.firmware: '1'
      PORTAL_SSID: 'MyWiFi'
      PORTAL_PASSPHRASE: 'MyWiFiPassword2022'
      BALENA_ARCH: rpi
      BALENA_MACHINE_NAME: raspberry-pi
      ENVIRONMENT_TARGET: arm-unknown-linux-gnueabihf

Dockerfile.template (contents):

FROM balenablocks/wifi-connect:rpi
WORKDIR /usr/src/app
COPY ui /usr/src/app/ui
COPY scripts/start.sh .
COPY target/arm-unknown-linux-gnueabihf/release/wifi-connect /usr/src/app/
CMD ["/bin/busybox","sh", "start.sh"]

So the start.sh script was not modified, it has the same content:

#!/usr/bin/env bash

export DBUS_SYSTEM_BUS_ADDRESS=unix:path=/host/run/dbus/system_bus_socket

# Optional step - it takes couple of seconds (or longer) to establish a WiFi connection
# sometimes. In this case, following checks will fail and wifi-connect
# will be launched even if the device will be able to connect to a WiFi network.
# If this is your case, you can wait for a while and then check for the connection.
# sleep 15

# Choose a condition for running WiFi Connect according to your use case:

# 1. Is there a default gateway?
# ip route | grep default

# 2. Is there Internet connectivity?
# nmcli -t g | grep full

# 3. Is there Internet connectivity via a google ping?
# wget --spider http://google.com 2>&1

# 4. Is there an active WiFi connection?
iwgetid -r

if [ $? -eq 0 ]; then
    printf 'Skipping WiFi Connect\n'
    printf 'Starting WiFi Connect\n'

# Start your application here.
sleep infinity

Still I have the following questions:

  1. Where is the Captive Portal created?
    – As far as I have seen that the entire WiFi Connect component its structured within the following:

A rust API interface (networks & connect endpoints)
– The backend has dnsmasq and at the very first request it does the redirect, using the following code:

struct RedirectMiddleware;

impl AfterMiddleware for RedirectMiddleware {
    fn catch(&self, req: &mut Request, err: IronError) -> IronResult<Response> {
        let gateway = {
            let request_state = get_request_state!(req);
            format!("{}", request_state.gateway)

        if let Some(host) = req.headers.get::<headers::Host>() {
            if host.hostname != gateway {
                let url = Url::parse(&format!("http://{}/", gateway)).unwrap();
                return Ok(Response::with((status::Found, Redirect(url))));


A react native application, that handles the UI part and webview part, here I was thinking to redirect the browser to this:

const onConnect = (data: NetworkInfo) => {

	fetch('/connect', {
		method: 'POST',
		body: JSON.stringify(data),
		headers: {
			'Content-Type': 'application/json',
		.then((resp) => {
			if (resp.status !== 200) {
				throw new Error(resp.statusText);
			if (resp.status == 200) {
				window.location.href = resp.headers.get("redirect-url")?? "customurl://aaa-123";
		.catch((e: Error) => {
			setError(`Failed to connect to the network. ${e.message || e}`);
  1. Where I can add to the header response on the endpoint of setting the wifi & password of a local connection?
    – So I would like to add to the header (redirect-url), not sure where the connect part is handled. Here?

Hi there @mpous ,

I think I picked the wrong lead due to the fact that I was missing the WiFi Connect diagram.

The issues is that the React (UI part of the Captive Portal) sends only the credentials to the WiFi (SSID & Password) and on the server side (RUST) the captive portal is stopped forcefully

Although I have created a reply on this thread since its more related to the actual context and hope I’d get attention of josephroberts’s kind interest to take a look and offer me an input about that :pleading_face:

After multiple tryouts and wrong leads I know that I only need to figure out how to avoid the rust to kill the captive portal just before getting the response from the /connect endpoint.

1 Like

Great that you finally found the right path @elsevero ! Let’s follow up the conversation there :slight_smile: :crossed_fingers: