Archive for the ‘Uncategorized’ Category

Goal: Make OpenCloud available locally (behind a NAT-router) and from outside.
Use docker with Traefik; with ipv4 and ipv6.
Only OpenCloud is accessible from outside; other https services running as docker containers are only available from inside.

Required (using QTS 5.2.10):

  • cross compile linux kernel module ip6table_nat.ko (reference) and load it by autorun.sh
  • edit .qpkg/container-station/etc/docker.json and set
    “ip6tables”: true
  • split dns to access opencloud from inside and outside

Used:

  • Dockhand to manage docker containers and stacks
  • Filebrowser Quantum to edit config files
    (version 2.1.3-beta was used to write this blog post)
  • ssh access for qnap admin user
  • mc as text editor when using ssh
  • Entware to install mc
  • Let’s Encrypt wildcard certificates created by Traefik using DNS-01 Challenge
  • a DNS-Provider supported by lego (ACME client and ACME library used by Traefik)
  • configure Traefik with multiple https EntryPoints
  • a dynamic dns provider with support for ipv6 (AAAA records)
  • a DNS server with A records pointing to YOURDOMAIN.TLD and to *.YOURDOMAIN.TLD with the ipv4 address of qnap ts-216g

Detailed Steps

Warning: The configuration described below is meant for a local network with trusted users!

  • The Traefik dashboard and Dockhand, the docker management web interface run without password protection.

QNAP-Configuration

  • Enable ipv6 (controlpanel/network/adapter): stateless, slaac without secret key. (Make sure your router firewall is blocking ipv6 access from outside)
  • Enable ssh
  • Enable admin user; (this asks for admin password; keep a secure copy of this password.)
  • Create (non admin) user container
    • which gets uid:gid=1001:100, if this is the first user created after running setup
    • to display uid, gid run by ssh command:
      id container
  • Enable autorun.sh (controlpanel/hardware)
  • Install Entware:
    • Download Entware
    • App Center / “+” (upper right corner) / Install manually
    • login as admin by ssh: opkg update; opk install mc
  • Install ContainerStation with default shared folder /Container
    • configure ContainerStation with default settings
    • with QTS 5.2.10 (ipv6 is not available as builtin feature)
    • docker compose files are located in subfolders of
      /share/Container/container-station-data/application/
  • Move https qnap webadmin to port 4443 (controlpanel/system/general settings)

Docker ipv6 on QNAP

  • ssh into qnap and download the compiled version of ip6table_nat.ko
    (kernel version 5.10.60-qnap; md5 sum 8bca1a42748535c60a0ca0835a766b8c):
    wget https://spblinux.de/qnap/ts-216g/ip6table_nat.ko
    (If you prefer to compile this module yourself, see instructions at the bottom of this page.)
    • modprobe ip6tables.ko
    • insmod ./ip6table_nat.ko
  • If both modules load successfully docker ipv6 can be enabled.
    • use mc to navigate to
      /share/CACHEDEV1_DATA/.qpkg/container-station/etc/
    • and edit docker.json
      • replace: “ip6tables”: false
      • by: “ip6tables”: true
    • restart ContainerStation
      • /etc/init.d/container-station.sh stop
      • /etc/init.d/container-station.sh start
  • Create ipv6 enabled docker network traefik-net
    • docker network create –ipv6 traefik-net
    • (if command docker is not found, exit from ssh and login again.)
    • If traefik-net has been created successfully docker runs with ipv6 support:
      • docker network inspect traefik-net
  • Make docker ipv6 support permanent:
    • mount the config partition of qnap flash storage
      • /etc/init.d/init_disk.sh mount_flash_config
      • cd /tmp/nasconfig_tmp/
      • touch autorun.sh
      • chmod +x autorun.sh
      • copy ip6table_nas.ko to /tmp/nasconfig_tmp/
      • open autorun.sh with mc editor and paste the code below into this file
      • save autorun.sh
      • /etc/init.d/init_disk.sh umount_flash_config
      • make sure autorun.sh is enabled in controlpanel/hardware
      • reboot the qnap nas and check if ip6table_nat.ko has been loaded
        • lsmod | grep ip6
        • docker network inspect traefik-net
#!/bin/sh
#
# to edit this file use:
#   /etc/init.d/init_disk.sh mount_flash_config
#   edit /tmp/nasconfig_tmp/autorun.sh
#   /etc/init.d/init_disk.sh umount_flash_config
#
modprobe ip6_tables
[ -d /tmp/nasconfig_tmp2 ] || mkdir /tmp/nasconfig_tmp2
mount /dev/mmcblk0p5  /tmp/nasconfig_tmp2
[ -f /tmp/nasconfig_tmp2/ip6table_nat.ko ] && insmod /tmp/nasconfig_tmp2/ip6table_nat.ko
umount /dev/mmcblk0p5  /tmp/nasconfig_tmp2

Install Traefik with socketproxy

Use ssh as admin user:

docker-compose.yml
(replace 192.168.224.252 by your qnap ipv4 address)

services:
  traefik:
    env_file:
      - path: /share/Container/container-station-data/application/traefik/.env
    image: traefik:v3.7
    container_name: traefik
    restart: always
    ports:
      # web, webs is set as default
      - 192.168.224.252:80:80/tcp # web
      - 192.168.224.252:443:443/tcp # webs
      - 192.168.224.252:443:443/udp # https http3 quic: webs
      # label: - traefik.routers.ROUTERNAME.entrypoints=web2,webs2
      # - 192.168.226.252:80:81/tcp # web2
      - 192.168.224.252:65480:81/tcp # web2, limit to 1 network adapter
      #- 192.168.226.252:443:444/tcp # webs2
      - 192.168.224.252:65443:444/tcp # webs2, limit to 1 network adapter
       # label: - traefik.routers.ROUTERNAME.entrypoints=web2,webs2,webs3
    - "[::]:443:445/tcp" # webs3, limit to ipv6, all network adapters
      #- 192.168.226.252:443:444/udp # https http3 quic: webs2
      - 8081:8080 # http api dashboard; also at https://YOURDOMAIN.TLD
    volumes:
      #- /var/run/docker.sock:/var/run/docker.sock:ro # better use socket-proxy instead
      - /share/Container/container-station-data/application/traefik:/etc/traefik/ # put the provided traefik.yml and fileConfig.yml files at this location
      - /share/Container/container-station-data/application/traefik/logs:/logs
    environment:
      - TZ=Europe/Zurich
    labels:
      - traefik.enable=true
      - traefik.http.routers.api.rule=Host(`YOURDOMAIN.TLD`) # Define the (sub)domain for the traefik dashboard.
      - traefik.http.routers.api.service=api@internal # Enable Traefik API.
      - traefik.http.routers.traefik.middlewares=rate-limit@file
      #- traefik.http.routers.api.middlewares=local-ipwhitelist@file,basic-auth@file # protect dashboard with basic auth and restrict access to private class subnets only
      #- traefik.http.middlewares.basic-auth-global.basicauth.users=admin:$$apr1$$epoKf5li$$QfTMJZOCS/halv3CiIUEu0 # protect the traefik dashboard by basic auth (pw=password)
    # extra_hosts:
    #  - host.docker.internal:172.17.0.1
    security_opt:
      - no-new-privileges:true
    networks:
      - traefik-net
      - socketproxy-net
  socket-proxy:
    image: lscr.io/linuxserver/socket-proxy:latest
    container_name: socket-proxy
    environment:
      - CONTAINERS=1
      - EVENTS=1
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro
    networks:
      - socketproxy-net
    restart: always
    labels:
      - com.centurylinklabs.watchtower.enable=false
    read_only: true
    tmpfs:
      - /run
networks:
  traefik-net:
      external: true # docker network ls; docker network create --ipv6 traefik-net
  socketproxy-net:    
    internal: true

traefik.yml

# Traefik global configuration
global:
  checkNewVersion: true
  sendAnonymousUsage: false

# Enable traefik ui dashboard
api:
  dashboard: true
  insecure: true

# Log level INFO|DEBUG|ERROR
log:
  level: INFO

# crowdsec bouncer
#experimental:
#  plugins:
#    bouncer:
#      moduleName: github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin
#      version: v1.7.1

#metrics:
#  influxDB2:
#     address: http://influxdb2:8086
#     token: my-secure-token-secret
#     org: influx-org
#     bucket: influx-bucket
#     addEntryPointsLabels: true
#     addRoutersLabels: true
#     addServicesLabels: true
#     pushInterval: 60s  

# Configuring Multiple Filters
accessLog:
  filePath: "/logs/traefik.log"
  format: json
  filters:
    statusCodes:
      - "200-299" # log successful http requests
      - "300-399" # log redirect http requests
      - "400-599" # log failed http requests
    #retryAttempts: true
    #minDuration: "10ms"
  # collect logs as in-memory buffer before writing into log file
  bufferingSize: 0
  fields:
    headers:
      defaultMode: drop # drop all headers per default
      names:
          User-Agent: keep # log user agent strings

# The setting below is to allow insecure backend connections.  
serverTransport:
  insecureSkipVerify: true

# Traefik entrypoints (network ports) configuration
entryPoints:
#    asDefault: true # in use unless changed by label traefik.http.routers.ROUTERNAME.entrypoints=web,webs
  # Not used in apps, but redirect everything from HTTP to HTTPS
  web:
    asDefault: true
    address: :80
#    forwardedHeaders:
    http:
      # https://doc.traefik.io/traefik/migrate/v3/#underscoreheadersstrategy
      underscoreHeadersStrategy: delete
      redirections:
        entryPoint:
          to: webs
          scheme: https
  web2:
    address: :81
#    forwardedHeaders:
    http:
      # https://doc.traefik.io/traefik/migrate/v3/#underscoreheadersstrategy
      underscoreHeadersStrategy: delete
      redirections:
        entryPoint:
          to: webs2
          scheme: https
  web3:
    address: "[::]:82"
#    forwardedHeaders:
    http:
      # https://doc.traefik.io/traefik/migrate/v3/#underscoreheadersstrategy
      underscoreHeadersStrategy: delete
      redirections:
        entryPoint:
          to: webs3
          scheme: https

  # HTTPS endpoint, with domain wildcard
  webs:
    asDefault: true
    address: :443
    forwardedHeaders:
      # reuse list of Cloudflare Trusted IP's above for HTTPS requests
#      trustedIPs: *trustedIps
    # enable HTTP3 QUIC via UDP/443
    http3:
      advertisedPort: '443'      
    http:
      # https://doc.traefik.io/traefik/migrate/v3/#underscoreheadersstrategy
      aliasHeadersStrategy: delete
      # use dynamically generated self-signed certificate
      #tls: {}
      # or use a pre-defined certificate resolver from below
      # if so, remove above line with tls: {}
      tls:
        certResolver: myresolver-dns
        domains:
          - main: YOURDOMAIN.TLD # change this to your proxy domain
            sans:
              - '*.YOURDOMAIN.TLD' # change this to your proxy domain
      middlewares:
        - security-headers@file # reference to a dynamic middleware for setting http security headers per default
  webs2:
    address: :444
    forwardedHeaders:
      # reuse list of Cloudflare Trusted IP's above for HTTPS requests
#      trustedIPs: *trustedIps
    # enable HTTP3 QUIC via UDP/443
    #http3:
    #  advertisedPort: '444'      
    http:
      # https://doc.traefik.io/traefik/migrate/v3/#underscoreheadersstrategy
      aliasHeadersStrategy: delete
      # use dynamically generated self-signed certificate
      #tls: {}
      # or use a pre-defined certificate resolver from below
      # if so, remove above line with tls: {}
      tls:
        certResolver: myresolver-dns
        domains:
          - main: YOURDOMAIN.TLD # change this to your proxy domain
            sans:
              - '*.YOURDOMAIN.TLD' # change this to your proxy domain
      middlewares:
        - security-headers@file # reference to a dynamic middleware for setting http security headers per default
  webs3:
    address: "[::]:445"
    forwardedHeaders:
      # reuse list of Cloudflare Trusted IP's above for HTTPS requests
#      trustedIPs: *trustedIps
    # enable HTTP3 QUIC via UDP/443
    #http3:
    #  advertisedPort: '444'      
    http:
      # https://doc.traefik.io/traefik/migrate/v3/#underscoreheadersstrategy
      aliasHeadersStrategy: delete
      # use dynamically generated self-signed certificate
      #tls: {}
      # or use a pre-defined certificate resolver from below
      # if so, remove above line with tls: {}
      tls:
        certResolver: myresolver-dns
        domains:
          - main: YOURDOMAIN.TLD # change this to your proxy domain
            sans:
              - '*.YOURDOMAIN.TLD' # change this to your proxy domain
      middlewares:
        - security-headers@file # reference to a dynamic middleware for setting http security headers per default

providers:
  providersThrottleDuration: 2s

  # File provider for connecting things that are outside of docker / defining middleware
  file:
    filename: /etc/traefik/fileConfig.yml
    watch: true

  # Docker provider for connecting all apps that are inside of the docker network
  docker:
    watch: true
    network: traefik-net # add Your Docker Network Name Here
    endpoint: "tcp://socket-proxy:2375" # use socket-proxy for secure access to docker api
    defaultRule: "Host(`{{ index .Labels \"com.docker.compose.service\"}}.YOURDOMAIN.TLD`)" # subdomain==service name; change 'YOURDOMAIN.TLD' to your proxy domain
#    defaultRule: "Host(`{{ .Name }}.YOURDOMAIN.TLD`)" # subdomain==SERVICENAME-CONTAINERNAME; change 'YOURDOMAIN.TLD' to your proxy domain
    exposedByDefault: false # only containers with label traefik.enable=true are routed by traefik

# Use letsencrypt to generate ssl certificates
certificatesResolvers:
  # ACME HTTP CHALLENGE
  myresolver-http:
    acme:
      email: osth@sunrise.ch  # the email address used for ssl certificate registration
      storage: /etc/traefik/acme.json
      httpChallenge: # acme http challenge; requires port 80 and proper dns entries
        entryPoint: web # specify the entry point for the HTTP challenge (adjust if needed)
  # ACME DNS CHALLENGE
  myresolver-dns:
    acme:
      email: YOU@MAIL.ORG  # the email address used for ssl certificate registration
      storage: /etc/traefik/acme.json
      dnsChallenge: # acme dns challenge; requires api token of dns provider
#        provider: YOURDNSHOST2
        provider: desec
        resolvers:
          - "1.1.1.1:53"
          - "1.0.0.1:53"

fileConfig.yml

http:

  ## EXTERNAL ROUTING EXAMPLE - Only use if you want to proxy something manually ##
#  routers:
#    test3:
#      entryPoints:
#        - https
#        - http
#      rule: 'Host(`test3.YOURDOMAIN.TLD`)'
#      service: test3
#      middlewares:
#        - "local-ipwhitelist@file"
  #routers:
  #  homeassistant:
  #    entryPoints:
  #      - https
  #      - http
  #    rule: 'Host(`ha.example.com`)'
  #    service: homeassistant
  #    middlewares:
  #      - "local-ipwhitelist@file"

  #  pve:
  #    entryPoints:
  #      - https
  #      - http
  #    rule: 'Host(`pve.example.com`)'
  #    service: pve
  #    middlewares:
  #      - "local-ipwhitelist@file"

  ## SERVICES EXAMPLE ##
#  services:
#     test3:
#       loadBalancer:
#         serversTransport: insecureTransport
#         servers:
#           - url: https://t4.YOURDOMAIN.TLD
  #  homeassistant:
  #    loadBalancer:
  #      serversTransport: insecureTransport
  #      servers:
  #        - url: http://192.168.1.10:8123

  #  pve:
  #    loadBalancer:
  #      serversTransport: insecureTransport
  #      servers:
  #        - url: https://192.168.1.20:8006

  # allow self-signed certificates for proxied web services
  serversTransports:
    insecureTransport:
      insecureSkipVerify: true

  ## MIDDLEWARES ##
  middlewares:
    # UNUSED remove X-Frame-Options header by plugin because of only office
    #custom-allow-iframe:
    #  plugin:
    #    htransformation:
    #      Rules:
    #        - Header: 'X-Frame-Options'
    #          Name: 'allow iframe'
    #          Type: 'Del'
    #          SetOnResponse: 'True'

    # Only Allow Local networks
    local-ipwhitelist:
      ipAllowList:
        sourceRange:
          - 127.0.0.1/32 # localhost
          - 10.0.0.0/8 # private class A
          - 172.16.0.0/12 # private class B
          - 192.168.0.0/16 # private class C
        #ipstrategy: # enable this when cloudflare proxy in use
        #  depth: 1 # enable this when cloudflare proxy in use

    # Security headers
    security-headers:
      headers:
        customResponseHeaders: # field names are case-insensitive
          #X-Robots-Tag: "all,noarchive,nosnippet,notranslate,noimageindex"
          Server: "" # prevent version disclosure
          X-Powered-By: "" # prevent version disclosure
          X-Forwarded-Proto: "https"
          #Permissions-Policy: "accelerometer=(), autoplay=(), camera=(), display-capture=(), encrypted-media=(), fullscreen=(), gamepad=(), geolocation=(), gyroscope=(), hid=(), identity-credentials-get=(), idle-detection=(), local-fonts=(), magnetometer=(), microphone=(), midi=(), otp-credentials=(), payment=(), picture-in-picture=(), publickey-credentials-get=(), screen-wake-lock=(), serial=(), storage-access=(), usb=(), web-share=(), window-management=(), xr-spatial-tracking=()"
          #Cross-Origin-Embedder-Policy: "unsafe-none"
          #Cross-Origin-Opener-Policy: "same-origin"
          #Cross-Origin-Resource-Policy: "same-site"
          #X-Permitted-Cross-Domain-Policies: "none"
        sslProxyHeaders:
          X-Forwarded-Proto: "https"
        hostsProxyHeaders:
          - "X-Forwarded-Host"
        #customRequestHeaders:
        #  X-Forwarded-Proto: "https"
        contentTypeNosniff: true # X-Content-Type-Options
        customFrameOptionsValue: "SAMEORIGIN" # X-Frame-Options
        browserXssFilter: false # X-XSS-Protection; deprecated
        referrerPolicy: "strict-origin-when-cross-origin" # Referrer-Policy
        forceSTSHeader: true # HTTP-Strict-Transport-Security (HSTS)
        stsIncludeSubdomains: true # HTTP-Strict-Transport-Security (HSTS)
        stsSeconds: 63072000 # HTTP-Strict-Transport-Security (HSTS)
        stsPreload: true # HTTP-Strict-Transport-Security (HSTS)
        #contentSecurityPolicy: "default-src 'self'; form-action 'self'; object-src 'none'; frame-ancestors 'none'; upgrade-insecure-requests; block-all-mixed-content" # Content-Security-Policy (CSP)

    # Authelia guard
    #authelia:
    #  forwardauth:
    #    address: http://authelia:9091/api/authz/forward-auth # replace example.com with your domain name
    #    trustForwardHeader: true
    #    authResponseHeaders:
    #      - Remote-User
    #      - Remote-Groups
    #      - Remote-Name
    #      - Remote-Email

    #crowdsec:
    #  plugin:
    #    bouncer:
    #      enabled: true
    #      defaultDecisionSeconds: 60
    #      crowdsecMode: live
    #      crowdsecAppsecEnabled: false
    #      crowdsecAppsecHost: crowdsec:7422
    #      crowdsecAppsecFailureBlock: true
    #      crowdsecAppsecUnreachableBlock: true
    #      crowdsecLapiKey: FIXME-LAPI-KEY # <--- replace this later
    #      crowdsecLapiHost: crowdsec:8080
    #      crowdsecLapiScheme: http
    #      crowdsecLapiTLSInsecureVerify: false
    #      banFilePath: /ban.html
    #      captchaProvider: hcaptcha
    #      captchaSiteKey: YourSiteKey
    #      captchaSecretKey: YourSecretKey
    #      captchaGracePeriodSeconds: 1800
    #      captchaFilePath: /captcha.html    
    #      forwardedHeadersTrustedIPs:
    #        # private class ranges
    #        - 10.0.0.0/8
    #        - 172.16.0.0/12
    #        - 192.168.0.0/16
    #        ## cloudflare ipv4
    #        #- 103.21.244.0/22
    #        #- 103.22.200.0/22
    #        #- 103.31.4.0/22
    #        #- 104.16.0.0/13
    #        #- 104.24.0.0/14
    #        #- 108.162.192.0/18
    #        #- 131.0.72.0/22
    #        #- 141.101.64.0/18
    #        #- 162.158.0.0/15
    #        #- 172.64.0.0/13
    #        #- 173.245.48.0/20
    #        #- 188.114.96.0/20
    #        #- 190.93.240.0/20
    #        #- 197.234.240.0/22
    #        #- 198.41.128.0/17
    #        ## cloudflare ipv6
    #        #- 2400:cb00::/32
    #        #- 2606:4700::/32
    #        #- 2803:f800::/32
    #        #- 2405:b500::/32
    #        #- 2405:8100::/32
    #        #- 2a06:98c0::/29
    #        #- 2c0f:f248::/32
    #      clientTrustedIPs:
    #        # private class ranges
    #        - 10.0.0.0/8
    #        - 172.16.0.0/12
    #        - 192.168.0.0/16

    # rate limiting
    rate-limit:
      rateLimit:
        average: 50
        period: 1s
        burst: 100
        # uncomment the next lines if behind cloudflare
        #sourceCriterion:
        #  requestHeaderName: CF-Connecting-IP        
    onlyoffice-ratelimit:
      rateLimit:
        average: 200
        period: 1s
        burst: 400


    # basic auth popup
    basic-auth:
      basicAuth:
        # https://hostingcanada.org/htpasswd-generator/
        users: "admin:$apr1$76boeqjp$tVQUCSSQky300AS.kgbZX/" # admin:password

# Only use secure ciphers - https://ssl-config.mozilla.org/#server=traefik&version=2.9&config=intermediate&guideline=5.6
tls:
  options:
    default:
      #sniStrict: true # prevents leaking default cert; see https://doc.traefik.io/traefik/v2.2/https/tls/#strict-sni-checking
      minVersion: VersionTLS12
      cipherSuites:
        - TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256
        - TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256
        - TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384
        - TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384
        - TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305
        - TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305

  • use ssh and
    • cd /share/Container/container-station-data/application/traefik
    • touch .env
    • put your dns credentials into this file, e.g.
      DESEC_TOKEN=’123456789abcdefgh’
    • reference
  • try to start traefik
    • cd /share/Container/container-station-data/application/traefik
    • docker compose up -d
    • docker compose logs -f
      (ctrl + c to stop log viewer)
    • on the first run it will take a while until traefik gets certificates from Let’s Encrypt
  • open https://YOURDOMAIN.TLD

Install Dockhand

  • use ssh and
    • cd /share/Container/container-station-data/application/
    • mkdir dockhand
    • cd dockhand
    • touch .env
    • use mc editor to write into file .env
      DATA_DIR=/share/Container/container-station-data/application/dockhand
      STACKS_DIR=/share/Container/container-station-data/application
    • touch docker-compose.yml
      and enter the code below into file docker-compose.yml
    • docker compose up -d
    • docker compose logs -f
    • open https://dockhand.YOURDOMAIN.TLD and add environment “/var/run/docker.sock”
    • use Dockhand to create other (docker compose) Stacks
services:
  dockhand:
    env_file:
      - path: /share/Container/container-station-data/application/dockhand/.env
    image: fnsys/dockhand:latest
    container_name: dockhand
    restart: unless-stopped
    user: 0:0
    ports:
      - 127.0.0.1:3000:3000
    # use https://dockhand.YOURDOMAIN.TLD   
    networks:
      - traefik-net
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
      - dockhand_data:/app/data
      - /share/Container/container-station-data/application/dockhand:/share/Container/container-station-data/application/dockhand
      - /share/Container/container-station-data/application:/share/Container/container-station-data/application
    labels:
      - traefik.enable=true
      - traefik.http.services.dockhand.loadbalancer.server.port=3000
      # Assign middleware: default rate-limit,...
      - traefik.http.routers.dockhand.middlewares=rate-limit@file
volumes:
  dockhand_data: null
networks:
  traefik-net:
    external: true

First test case: hello world with nginx

  • logged in by ssh
    • cd /share/Container/container-station-data/application
    • mkdir test1
    • cd test1
    • mkdir html
    • echo “hello world” >html/index.html
    • mkdir etcnginxconfd
    • touch etcnginxconfd/test1.conf
    • use mc to edit test1.conf:
      server {
      listen 80;
      server_name: test1.YOURDOMAIN.TLD
      index index.html;
      root /usr/share/nginx/html;
      }
  • Use Dockhand / Stacks and create a new stack “test1”
    • the content of compose.yaml is shown below
  • “Create & Start” this stack
    and open https://test1.YOURDOMAIN.TLD
  • Debugging:
    • Dockhand / Stacks allows to view the logs
    • Dockhand / Containers allows to open a shell inside a running container
    • https://YOURDOMAIN.TLD: Traefik / HTTP Routers has to show
      Host(`test1.YOURDOMAIN.TLD`) with Entrypoint webs

compose.yaml:

services:
  test1: 
    image: nginx:alpine
    # ports:
    #  - "127.0.0.1:8083:80"
    # use https://test1.YOURDOMAIN.TLD
    networks:
      - traefik-net
    environment:
      - APP_ENV=${APP_ENV:-production}
    volumes:
#      - /share/YOURQNAPFOLDER/opt/test1/html:/usr/share/nginx/html:ro
      - /share/Container/container-station-data/application/test1/html:/usr/share/nginx/html:ro
      - /share/Container/container-station-data/application/test1/etcnginxconfd:/etc/nginx/conf.d
    restart: unless-stopped
    labels:
      - traefik.enable=true
      - traefik.http.services.test1.loadbalancer.server.port=80
      # - traefik.http.routers.test1.rule=Host(`t1.YOURDOMAIN.TLD`) # override default == SERVICENAME
      # Assign middleware: default rate-limit,...
      - traefik.http.routers.test1.middlewares=rate-limit@file
      - traefik.http.routers.test1.entrypoints=web,webs

networks:
   traefik-net:
     external: true

Testing external access with ipv4

  • ipv4 external access uses traefik entrypoint webs2 (traefik.yml)
    • which listens inside traefik-net on port 444
    • traefik/docker-compose.yml forwards this to QNAP_IP:65443
    • the nat-router has to forward internet ipv4 port 443 to QNAP_IP:65443
    • test1/compose.yaml defines if entrypoint webs2 is used by container test1:
      labels:
      – traefik.http.routers.test1.entrypoints=web,webs,webs2
    • click “Save & redeploy” if compose.yaml has been edited by Dockhand
    • traefik webinterface (https://YOURDOMAIN.TLD) shows in section routers, if test1.YOURDOMAIN.TLD is using webs2
  • test ipv4 external with linux:
    • required is a linux computer with internet access which is not connected to the local lan of QNAP_IPV4_ADDRESS
    • edit /etc/hosts and add the line:
      QNAP_IPV4_ADDRESS test1.YOURDOMAIN.TLD
    • wget -4 https:test1.YOURDOMAIN.TLD
    • this should fetch index.html
    • content of file index.html should be
      hello world
    • remove (or comment out with #) the line QNAP_IPV4_ADDRESS in /etc/hosts
  • do verify that https:test1.YOURDOMAIN.TLD is no longer accessible externally by ipv4 if webs2 is removed from labels
    • – traefik.http.routers.test1.entrypoints=web,webs

Testing external access with ipv6

  • ipv6 external access uses traefik entrypoint webs3 (traefik.yml)
    • which listens inside traefik-net on port 445
    • traefik/docker-compose.yml forwards this to ANY_IPV6_ADDRESS_ON_QNAP:443
      (TS-216G has 2 lan ports; the prefix part of the (non local) ipv6 addresses might change dynamically; so it should not be written into traefik/docker-compose.yml)
    • the (nat-)router has to allow external access by ipv6 to QNAP_IPV6_ADDRESS port 443
    • test1/compose.yaml defines if entrypoint webs3 is used by container test1:
      labels:
      – traefik.http.routers.test1.entrypoints=web,webs,webs3
    • click “Save & redeploy” if compose.yaml has been edited by Dockhand
    • traefik webinterface (https://YOURDOMAIN.TLD) shows in section routers, if test1.YOURDOMAIN.TLD is using webs3
  • test ipv6 external with linux:
    • required is a linux computer with internet access which is not connected to the local lan of QNAP_IP
    • edit /etc/hosts and add the line:
      QNAP_IPV6_ADDRESS test1.YOURDOMAIN.TLD
    • wget -6 https:test1.YOURDOMAIN.TLD
    • this should fetch index.html
    • content of file index.html should be
      hello world
    • remove (or comment out with #) the line QNAP_IPV6_ADDRESS in /etc/hosts
  • do verify that https:test1.YOURDOMAIN.TLD is no longer accessible externally by ipv6 if webs3 is removed from labels
    • – traefik.http.routers.test1.entrypoints=web,webs

DNS record for external access

  • if the domain name given by your dyndns provider is
    YOU.YOURDYNDNSPROVIDER.TLD
  • and if this name has both an ipv4 A record and an ipv6 AAAA record,
  • then to the existing A records of your dns provider for YOURDOMAIN.TLD
    • YOURDOMAIN.TLD QNAP_IPV4_ADRESS
    • *.YOURDOMAIN.TLD QNAP_IPV4_ADRESS
  • a CNAME record has to be added
    • test1.YOURDOMAIN.TLD YOU.YOURDYNDNSPROVIDER.TLD
  • use dig or nslookup to test these settings
    • dig test1.YOURDOMAIN.TLD
    • dig test1.YOURDOMAIN.TLD AAAA
  • set both entrypoints, webs2 and webs3 in test1/compose.yaml:
    labels:
    – traefik.http.routers.test1.entrypoints=web,webs,webs2,webs3
  • finally open https://test1.YOURDOMAIN.TLD by browser on a computer which is not connected to your local lan.

Install Quantum Filebrowser as editor for container config files

  • logged in by ssh
    • cd /share/Container/container-station-data/application
    • mkdir filebrowser
    • cd filebrowser
    • mkdir data
    • touch config.yaml
    • use mc to edit config.yaml:
      the content of this file is shown below
  • Use Dockhand / Stacks and create a new stack “filebrowser”
  • the content of compose.yaml is shown below
  • “Create & Start” this stack
  • Use Dockhand / Stacks to display the log file of filebrowser
    • one of the first log lines displays the initial password for user admin
  • open https://filebrowser.YOURDOMAIN.TLD
    • login as user admin with password from log file
      and set your personal password;
      (filebrowser has write access to all docker compose stacks!)
  • The “home” directory of filebrowser points to
    /share/Container/container-station-data/application

config.yaml:

server:
  cacheDir: /home/filebrowser/data/tmp # using the data volume so it can persist across restarts
  sources:
    - path: /Docker # Do not use a root "/" directory or include the "/var" folder
      config:
        defaultEnabled: true
  logging:
    - output: "stdout"
      levels: "info|warning|error"  # add debug to see detailed logs

http:
  externalUrl: "https://filebrowser.YOURDOMAIN.TLD"
  internalUrl: "http://filebrowser:80"
  trustProxyHeaders: true   # v2.0.0+; on v1.4.x–v1.5.x use trustedHeaders list instead

#integrations:
#  office:
#    url: "https://onlyoffice.YOURDOMAIN.TLD"
#    internalUrl: "http://onlyoffice:80" # (optional)

compose.yaml:

services:
  filebrowser:
    image: gtstef/filebrowser:beta
    container_name: filebrowser
    user: 0:0 # running as user admin (like dockhand)
    volumes:
      - /share/Container/container-station-data/application:/Docker # Do not use a root "/" directory or include the "/var" folder
      - /share/Container/container-station-data/application/filebrowser/data:/home/filebrowser/data 
# IF config.yml is not in data/ but in /share/Container/container-station-data/application/filebrowser
# IT NEEDS extra volume for config.yaml
# and this location of config.yaml is defined by environment variable FILEBROWSER_CONFIG
      - /share/Container/container-station-data/application/filebrowser/config.yaml:/share/Container/container-station-data/application/filebrowser/config.yaml
    ports:
      - 127.0.0.1:8080:80 # exposes port 80 to host, the left-side number can be changed without config.yaml changes.
    # use https://filebrowser.YOURDOMAIN.TLD    
    restart: unless-stopped
    networks:
      - traefik-net
    environment:
      - FILEBROWSER_CONFIG=/share/Container/container-station-data/application/filebrowser/config.yaml
#      - FILEBROWSER_ONLYOFFICE_SECRET=${COMMON_SECRET} # set in .env file; created by openssl rand -base64 32
    labels:
      - traefik.enable=true
      - traefik.http.services.filebrowser.loadbalancer.server.port=80
      # Assign middleware: default rate-limit,required headers for onlyoffice integration
      - traefik.http.routers.filebrowser.middlewares=rate-limit@file,filebrowser-headers
      - traefik.http.middlewares.filebrowser-headers.headers.accessControlAllowOriginList=https://onlyoffice.YOURDOMAIN.TLD
      - traefik.http.middlewares.filebrowser-headers.headers.contentSecurityPolicy=frame-ancestors 'self' https://onlyoffice.YOURDOMAIN.TLD
networks:
  traefik-net:
      external: true

Install OpenCloud 7.2 stable

For some reason the docker container of OpenCloud does not include these config files (source):

apps.yaml

maps:
  config:
    folderViewEnabled: false

csp.yaml

directives:
  child-src:
    - '''self'''
  connect-src:
    - '''self'''
    - 'blob:'
    - 'https://${COMPANION_DOMAIN|companion.opencloud.test}${TRAEFIK_PORT_HTTPS}/'
    - 'wss://${COMPANION_DOMAIN|companion.opencloud.test}${TRAEFIK_PORT_HTTPS}/'
    - 'https://raw.githubusercontent.com/opencloud-eu/awesome-apps/'
    - 'https://${IDP_DOMAIN|keycloak.opencloud.test}${TRAEFIK_PORT_HTTPS}/'
    - 'https://update.opencloud.eu/'
    - 'https://tile.openstreetmap.org/'
  default-src:
    - '''none'''
  font-src:
    - '''self'''
  frame-ancestors:
    - '''self'''
  frame-src:
    - '''self'''
    - 'blob:'
    - 'https://embed.diagrams.net/'
    # In contrary to bash and docker the default is given after the | character
    - 'https://${COLLABORA_DOMAIN|collabora.opencloud.test}${TRAEFIK_PORT_HTTPS}/'
    - 'https://${EURO_OFFICE_DOMAIN|euro-office.opencloud.test}${TRAEFIK_PORT_HTTPS}/'
    # This is needed for the external-sites web extension when embedding sites
    - 'https://docs.opencloud.eu'
  img-src:
    - '''self'''
    - 'data:'
    - 'blob:'
    - 'https://raw.githubusercontent.com/opencloud-eu/awesome-apps/'
    - 'https://tile.openstreetmap.org/'
    # In contrary to bash and docker the default is given after the | character
    - 'https://${COLLABORA_DOMAIN|collabora.opencloud.test}${TRAEFIK_PORT_HTTPS}/'
    - 'https://${EURO_OFFICE_DOMAIN|euro-office.opencloud.test}${TRAEFIK_PORT_HTTPS}/'
  manifest-src:
    - '''self'''
  media-src:
    - '''self'''
  object-src:
    - '''self'''
    - 'blob:'
  script-src:
    - '''self'''
    - '''unsafe-inline'''
    - 'https://${IDP_DOMAIN|keycloak.opencloud.test}${TRAEFIK_PORT_HTTPS}/'
  style-src:
    - '''self'''
    - '''unsafe-inline'''
  worker-src:
    - "'self'"
    - 'blob:'

banned-password-list.txt

password
12345678
123
OpenCloud
OpenCloud-1

(If you want to follow the official setup instructions to downloads these config files into directory docker-compose/config/opencloud you have to install opkg install git git-http)

  • with qnap webinterface
    • create a shared folder YOURFOLDER
    • verify by ssh that this folder can be accessed by path
      /share/YOURFOLDER
    • give qnap user container read write access to this folder
      (opencloud container runs with uid:gid=1001:100=container:everyone)
    • verify by ssh that this folder can be accessed by path
      /share/YOURFOLDER
    • inside YOURFOLDER create the directories
      • opt
      • opt/opencloud
  • logged in by ssh configure shared folder YOURFOLDER
    • cd /share/YOURFOLDER/opt/opencloud
    • mkdir config
      (bind mounted configuration volume of opencloud; defined in .env by OC_CONFIG_DIR)
    • touch config/apps.yaml
    • touch config/csp.yaml
    • touch config/banned-password-list.txt
    • mkdir data
      (bind mounted data volume of opencloud; defined in .env by OC_DATA_DIR)
    • mkdir apps
      (bind mounted volume to install apps in opencloud; defined in .env by OC_APPS_DIR)
    • chown -R 1001:100 config data apps
    • use mc editor to fill apps.yaml, csp.yaml, banned-passwors-list.txt with the content shown above
  • logged in by ssh create the docker configuration files of opencloud (reference1, reference2)
    • cd /share/Container/container-station-data/application
    • mkdir opencloud
    • cd opencloud
    • touch docker-compose.yml
    • touch .env
    • touch dockhand.yml
    • mkdir external-traefik
    • touch external-traefik/opencloud.yml
  • Use Filebrowser to edit docker-compose.yml and external-traefik/opencloud.yml
    (see contents below)
  • Then use Dockhand / Stacks to create a new stack named opencloud and open dockhand.yml as compose file for Dockhand.
    • edit .env and dockhand.yml with Dockhand
      (see contents below)
    • Usage of dockhand.yml is a workaround because Dockhand does not support to merge multiple compose files by environment variable COMPOSE_FILE
  • Below are the contents of the configuration files.

docker-compose.yml:

services:
  opencloud:
    # renovate: depName=opencloudeu/opencloud-rolling
    image: ${OC_DOCKER_IMAGE:-opencloudeu/opencloud-rolling}:${OC_DOCKER_TAG:-7.5.0}
    # changelog: https://github.com/opencloud-eu/opencloud/tree/main/changelog
    # release notes: https://docs.opencloud.eu/opencloud_release_notes.html
    user: ${OC_CONTAINER_UID_GID:-0:0}
    networks:
      traefik-net:
    entrypoint:
      - /bin/sh
    # run opencloud init to initialize a configuration file with random secrets
    # it will fail on subsequent runs, because the config file already exists
    # therefore we ignore the error and then start the opencloud server
    command: ["-c", "opencloud init || true; opencloud server"]
#    command: ["-c", "sleep 36000"] # console access by dockhand while opencloud is not running: opencloud idm resetpassword
    environment:
      # enable services that are not started automatically
      OC_ADD_RUN_SERVICES: ${START_ADDITIONAL_SERVICES}
      OC_URL: https://${OC_DOMAIN:-cloud.opencloud.test}${TRAEFIK_PORT_HTTPS:+:}${TRAEFIK_PORT_HTTPS:-}
      OC_LOG_LEVEL: ${LOG_LEVEL:-info}
      OC_LOG_COLOR: "${LOG_PRETTY:-false}"
      OC_LOG_PRETTY: "${LOG_PRETTY:-false}"
      # do not use SSL between the reverse proxy and OpenCloud
      PROXY_TLS: "false"
      # INSECURE: needed if OpenCloud / reverse proxy is using self generated certificates
      OC_INSECURE: "${INSECURE:-false}"
      # basic auth (not recommended, but needed for eg. WebDav clients that do not support OpenID Connect)
      PROXY_ENABLE_BASIC_AUTH: "${PROXY_ENABLE_BASIC_AUTH:-false}"
      # demo users
      IDM_CREATE_DEMO_USERS: "${DEMO_USERS:-false}"
      # admin password
      IDM_ADMIN_PASSWORD: "${INITIAL_ADMIN_PASSWORD}"
      # email server (if configured)
      NOTIFICATIONS_SMTP_HOST: "${SMTP_HOST}"
      NOTIFICATIONS_SMTP_PORT: "${SMTP_PORT}"
      NOTIFICATIONS_SMTP_SENDER: "${SMTP_SENDER:-OpenCloud Notifications <notifications@cloud.opencloud.test>}"
      NOTIFICATIONS_SMTP_USERNAME: "${SMTP_USERNAME}"
      NOTIFICATIONS_SMTP_PASSWORD: "${SMTP_PASSWORD}"
      NOTIFICATIONS_SMTP_INSECURE: "${SMTP_INSECURE:-false}"
      NOTIFICATIONS_SMTP_AUTHENTICATION: "${SMTP_AUTHENTICATION}"
      NOTIFICATIONS_SMTP_ENCRYPTION: "${SMTP_TRANSPORT_ENCRYPTION:-none}"
      FRONTEND_ARCHIVER_MAX_SIZE: "10000000000"
      FRONTEND_CHECK_FOR_UPDATES: "${CHECK_FOR_UPDATES:-true}"
      PROXY_CSP_CONFIG_FILE_LOCATION: /etc/opencloud/csp.yaml
      # enable to allow using the banned passwords list
      OC_PASSWORD_POLICY_BANNED_PASSWORDS_LIST: banned-password-list.txt
      # control the password enforcement and policy for public shares
      OC_SHARING_PUBLIC_SHARE_MUST_HAVE_PASSWORD: "${OC_SHARING_PUBLIC_SHARE_MUST_HAVE_PASSWORD:-true}"
      OC_SHARING_PUBLIC_WRITEABLE_SHARE_MUST_HAVE_PASSWORD: "${OC_SHARING_PUBLIC_WRITEABLE_SHARE_MUST_HAVE_PASSWORD:-false}"
      OC_PASSWORD_POLICY_DISABLED: "${OC_PASSWORD_POLICY_DISABLED:-false}"
      OC_PASSWORD_POLICY_MIN_CHARACTERS: "${OC_PASSWORD_POLICY_MIN_CHARACTERS:-8}"
      OC_PASSWORD_POLICY_MIN_LOWERCASE_CHARACTERS: "${OC_PASSWORD_POLICY_MIN_LOWERCASE_CHARACTERS:-1}"
      OC_PASSWORD_POLICY_MIN_UPPERCASE_CHARACTERS: "${OC_PASSWORD_POLICY_MIN_UPPERCASE_CHARACTERS:-1}"
      OC_PASSWORD_POLICY_MIN_DIGITS: "${OC_PASSWORD_POLICY_MIN_DIGITS:-1}"
      OC_PASSWORD_POLICY_MIN_SPECIAL_CHARACTERS: "${OC_PASSWORD_POLICY_MIN_SPECIAL_CHARACTERS:-1}"
      # default language for services/WebUI; defaults to English, language code (ISO 639-1, e.g. de, en, fr)
      OC_DEFAULT_LANGUAGE: ${DEFAULT_LANGUAGE}
    volumes:
    # - ./config/opencloud/csp.yaml:/etc/opencloud/csp.yaml
    #  - ./config/opencloud/apps.yaml:/etc/opencloud/apps.yaml
    #  - ./config/opencloud/banned-password-list.txt:/etc/opencloud/banned-password-list.txt
      # configure the .env file to use own paths instead of docker internal volumes
      - ${OC_CONFIG_DIR:-opencloud-config}:/etc/opencloud
      - ${OC_DATA_DIR:-opencloud-data}:/var/lib/opencloud
      - ${OC_APPS_DIR:-./config/opencloud/apps}:/var/lib/opencloud/web/assets/apps
    logging:
      driver: ${LOG_DRIVER:-local}
    restart: always

volumes:
  opencloud-config:
  opencloud-data:

networks:
  traefik-net:
    external: true

dockhand.yml:

# dockhand start does not merge files using COMPOSE_FILE
# workaround: use this file dockhand.yml as stack compose file inside dockhand (browse to select it instead of docker-compose.yml)
# it *merges* the files with include (using path list)
# COMPOSE_FILE=docker-compose.yml:weboffice/euro-office.yml:external-traefik/opencloud.yml:external-traefik/euro-office.yml
include:
  - path:
    - ./docker-compose.yml
#    - ./weboffice/euro-office.yml
    - ./external-traefik/opencloud.yml
#    - ./external-traefik/euro-office.yml

external-traefik/opencloud.yml:

services:
  opencloud:
    environment:
      # bind to all interfaces
      PROXY_HTTP_ADDR: "0.0.0.0:9200"
    ports:
      # expose the opencloud server on localhost
      - "127.0.0.1:9200:9200"
    networks:
      - traefik-net
    labels:
      - traefik.enable=true
      - traefik.http.services.opencloud.loadbalancer.server.port=9200
      - traefik.http.routers.opencloud.rule=Host(`oc.YOURDOMAIN.TLD`) # Define the subdomain (defaults to SERVICENAME).
      - traefik.http.routers.opencloud.entrypoints=web,webs # default entrypoints web, webs
#      - traefik.http.routers.opencloud.entrypoints=web,webs,webs2 # webs with split dns+hairpin nat; webs2 external access (src nat)
#      - traefik.http.routers.opencloud.entrypoints=web,webs,webs2,webs3 # webs with split dns+hairpin nat; webs2 external access (src nat); webs3 ipv6
      - "traefik.http.middlewares.hsts-header.headers.stsSeconds=31536000"
      - "traefik.http.middlewares.hsts-header.headers.stsIncludeSubdomains=true"
      - "traefik.http.middlewares.hsts-header.headers.stsPreload=true"
      - "traefik.http.middlewares.hsts-header.headers.forceSTSHeader=true"
      # Assign middleware: default rate-limit,required headers for onlyoffice integration
      - traefik.http.routers.opencloud.middlewares=rate-limit@file,opencloud-headers,hsts-header
#      - traefik.http.middlewares.opencloud-headers.headers.accessControlAllowOriginList=https://euro-office.YOURDOMAIN.TLD,https://oc.YOURDOMAIN.TLD
#      - "traefik.http.middlewares.opencloud-headers.headers.contentSecurityPolicy=frame-ancestors 'self' https://euro-office.YOURDOMAIN.TLD"
      - traefik.http.middlewares.opencloud-headers.headers.accessControlAllowOriginList=https://oc.YOURDOMAIN.TLD
      - "traefik.http.middlewares.opencloud-headers.headers.contentSecurityPolicy=frame-ancestors 'self'"
#      - traefik.http.middlewares.opencloud-headers.headers.accessControlAllowOriginList=https://euro-office.YOURDOMAIN.TLD
#      - traefik.http.middlewares.opencloud-headers.headers.accessControlAllowOriginList=https://oc.YOURDOMAIN.TLD
# try to match issuer in https://oc.YOURDOMAIN.TLD/.well-known/openid-configuration: "issuer": "https://oc.YOURDOMAIN.TLD:443"
networks:
    traefik-net:
       external: true

.env (click on this link to show the content of this file)

First start of OpenCloud

  • logged in by ssh
    • cd /share/Container/container-station-data/application/opencloud
    • docker compose up -d
    • docker compose logs -f
      (use ctrl + c to stop logging)
  • webbrowser
    • open oc.YOURDOMAIN.TLD
    • the login page of Opencloud should open
    • user: admin
      password as defined in .env file by
      INITIAL_ADMIN_PASSWORD=YOUR1-2-3password
  • troubleshooting, if login fails with error 500
    • look for red errors (docker compose logs -f)
    • docker compose down
    • try to fix missing files
    • use mc to remove
      • config/opencloud.yaml
      • data/*
        (remove all subdirectories of data/)
    • docker compose up -d
  • use Dockhand / Stacks to stop and start opencloud
  • edit file external-traefik/opencloud.yml with Filebrowser:
    • traefik.http.routers.opencloud.entrypoints=… defines on which ports OpenCloud listens
      • web,webs: ipv4: 80 and 443 (http and https)
      • webs2: ipv4: 65443 (https)
      • webs3: ipv6: 443 (https)
  • web,webs are endpoints used inside the local network (Traefik Dashboard, Dockhand, Filebrowser, OpenCloud)
  • webs2 is meant to be accessed from external networks (OpenCloud)
  • webs3 is meant to be accessed from external networks by ipv6 (OpenCloud)

NAT-Router configuration

  • Most NAT-Routers do not have static external ip addresses.
    • dynamic dns is required
    • port 443 of the external ipv4 address has to be forwarded to port 66543 of the ipv4 address of qnap.
    • if ipv6 should be used the dynamic dns provider has to support AAAA records
    • usually a script running on the router is required which sets this AAAA record:
      The ipv6 address as a combination of
      • the ipv6 prefix given by your internet provider
      • the ipv6 suffix given by ipv6 address of qnap
    • the same script has to set the firewall rule which allows external access exclusively on port 443 to this ipv6 address.

Hints to compile linux kernel module ip6table_nat.ko

  • use an x64 installation of Ubuntu 26.04 to cross compile
  • follow this reference
  • cross compile kernel and module
    • apt-get install make build-essential libncurses-dev bison flex libssl-dev libelf-dev
    • export ARCH=arm64
    • export CROSS_COMPILE=/YOURPATH/aarch64-QNAP-linux-gnu/bin/aarch64-QNAP-linux-gnu-
    • cd /YOURPATH/GPL_QTS/src/linux-5.10/
    • cp ../../kernel_cfg/TS-X16/linux-5.10-arm64.config ./.config
    • make oldconfig
    • make menuconfig
      • Networking support > Networking options > Network packet filtering framework (Netfilter) > IPv6: Netfilter Configuration
        • <M>   ip6tables NAT support
    • make clean
    • make prepare
    • use the flags defined in GPL_QTS/src/linux-5.10/cflag_kernel_qnap.mk:
    • compile the kernel with CFLAGS=… make zImage -j8 (in case your cpu has 8 cores)
      • CFLAGS=”-DTSX55U -DQNAP -DX86_PARKER -DNAS_VIRTUAL -DNAS_VIRTUAL_EX -DQNAP_FNOTIFY -DQNAP_SEARCH_FILENAME_CASE_INSENSITIVE -DQNAP_HAL -DSUPPORT_VAAI -DQNAP_I2C_MV9235 -DSUPPORT_FAST_BLOCK_CLONE -DSUPPORT_LOGICAL_BLOCK_4KB_FROM_NAS_GUI -DSUPPORT_CONCURRENT_TASKS -DSUPPORT_SINGLE_INIT_LOGIN -DVIRTUAL_JBOD -DSUPPORT_VOLUME_BASED -DQTS_HA -DSUPPORT_TP -DNFS_VAAI -DNFS_VAAI_V3 -DNFS_QLOG -DQNAP_NFS_FORCE_UMASK -DQNAP_SNAPSHOT -DISCSI_MULTI_INIT_ACL -DUSE_BLKDEV_READPAGES -DUSE_BLKDEV_WRITEPAGES -DKSWAPD_FIX -DQNAP_KERNEL_STORAGE_V2″ make zImage -j8
    • WORKAROUND: comment out the compilation of modules of drivers/target because of compile errors
      • edit drivers/Makefile and comment out “target”:
        • #obj-$(CONFIG_TARGET_CORE)<—>+= target/
    • compile the modules with CFLAGS=… make modules -j8
      • CFLAGS=”-DTSX55U -DQNAP -DX86_PARKER -DNAS_VIRTUAL -DNAS_VIRTUAL_EX -DQNAP_FNOTIFY -DQNAP_SEARCH_FILENAME_CASE_INSENSITIVE -DQNAP_HAL -DSUPPORT_VAAI -DQNAP_I2C_MV9235 -DSUPPORT_FAST_BLOCK_CLONE -DSUPPORT_LOGICAL_BLOCK_4KB_FROM_NAS_GUI -DSUPPORT_CONCURRENT_TASKS -DSUPPORT_SINGLE_INIT_LOGIN -DVIRTUAL_JBOD -DSUPPORT_VOLUME_BASED -DQTS_HA -DSUPPORT_TP -DNFS_VAAI -DNFS_VAAI_V3 -DNFS_QLOG -DQNAP_NFS_FORCE_UMASK -DQNAP_SNAPSHOT -DISCSI_MULTI_INIT_ACL -DUSE_BLKDEV_READPAGES -DUSE_BLKDEV_WRITEPAGES -DKSWAPD_FIX -DQNAP_KERNEL_STORAGE_V2 -DMODULE” make modules -j8
    • module ip6table_nat.ko is in directory
      • net/ipv6/netfilter

finis

Backup with rsync on qnap nas (architecture armel). Inside a private protected local network rsync (3.2.7) is running in daemon mode. Together with btrkbk to store snapshots. Debian 12 is the latest version of debian which supports my nas (qnap trs219p).

Having updated debian os, 2026-07, rsync stopped working with files bigger than 2GB – rsync error: file too large.
(dpkg -l | grep rsync shows: rsync_3.2.7-1+deb12u6)

Going back to version rsync_3.2.7-1+deb12u4 removed this error. (Possible background: debian maintainers backported the patches described here.)

Steps to downgrade from rsync patch version u6 to u4:

  • as root user:
    • apt-get install build-essential fakeroot devscripts
    • apt build-dep rsync
  • as normal user build deb package for rsync:
    • mkdir src; cd src
    • mkdir rsync3271u4; cd rsync3271u4
    • look up url of dsc file for rsync_3.2.7-1+deb12u4.dsc here (snapshot.debian.org)
    • dget DSC_URL
    • cd rsync-3.2.7
    • debuild -b -uc -us
  • as root user:
    • tell debian apt to pin version u4 of rsync
    • create file /etc/apt/preferences.d/rsync
    • content of this file:
      Package: rsync
      Pin: version 3.2.7-1+deb12u4
      Pin-Priority: 1000
    • finally install rsync u4 from local deb package
    • dpkg -i rsync3271u4/rsync_3.2.7-1+deb12u4_armel.deb

Environment: Tablet (Android 11, 4GB RAM, 8 cores + keyboard with touchpad),
IDE VS Code (code-server 4.7+Volar+Vetur Extended, displayed by Chrome 104) running inside Termux

Steps to get the web browser based IDE VS Code with code-server up and running:

  • Install Termux from F-Droid (terminal emulator and Linux environment app)
    • due to permission issues since android 10 the Termux app on Google Play Store is outdated (reference)
    • security settings on Android: allow installation of apk
    • open Termux console and run shell command pkg update to update all components of termux
  • Install NodeJS inside Termux:
    • pkg install nodejs-lts
      • in my case I got EACCESS errors in /data/data/com.termux […] cacache/tmp which were resolved by temporarily switching from nodejs-lts to nodejs and back again:
        pkg uninstall nodejs-lts
        pkg install nodejs
        • a true solution would be to chown files in tmp dir of npm to the current user:
          • npm get tmp shows the tmp directory of npm
          • id shows user and group id of current user (uid, gid)
          • chown -R 12345:12345 PATH_TO_TMP/tmp/
            chown -R 12345:12345 tmp/
        • but Termux runs as restricted user and cannot change ownership
      • code-server, currently (2022-09), requires nodejs-lts
  • Install dependencies to be able to build code-server
  • Use npm to install on global scope code-server
    • npm install -g code-server –unsafe-permissions (reference)
    • if code-server got installed but does not run because of missing modules (minimist, yauzl, yazl) then cd into /data/data/com.termux/files/usr/lib/node_modules/code-server/lib/vscode and run npm install –legacy-peer-deps (reference)
  • Run code-server
    • code-server
      • either edit $HOME/.config/code-server/config.yaml and set your password
      • or run code-server –auth none (reference)
    • open http://localhost:8080 with your web browser
    • install extensions Volar and Vetur extended from code-server web interface
  • Create a NuxtJS example app (reference)
    • use second session in Termux
      • open a second terminal inside Termux (keyboard shortcuts)ctrl+alt+c creates a new session
        • ctrl+alt+p or ctrl+alt+down arrow goes to previous session
        • ctrl+alt+n or ctrl+alt+up arrow goes to next session
        • ctrl+alt+right arrow opens session menu
        • (left arrow closes it; up/down switches sessions)
    • run yarn create nuxt-app hello-nuxt
      • this command does automatically install required packages
      • when asked:
        use java-script/packager yarn/no UI framework/no modules/no linting/no testing
        render universal (SSR / SSG)/deploy static/jamstack/no version control
      • cd into project directory hello-nuxt and run yarn dev
      • (later use yarn build and yarn start for production version;
        these commands are defined in the scripts section of file package.json.)
    • open http://localhost:3000
    • open project directory $HOME/hello-nuxt with explorer of code-server
      • edit pages/index.vue and save the changes
    • these changes, when saved, are immediately shown at http://localhost:3000
    • use copy and paste to replace index.vue by code of the NuxtJS hello world example code
    • open left pane of code sandbox at hello world example code and copy code of about.vue
    • create file about.vue inside explorer of code-server, paste code and save file

simple-code-editor is another example

  • goto home directory and generate the nuxt framework with command
    yarn create nuxt-app nuxt-example2
    • accept defaults when asked, just press enter, except Deployment target: use Static and no Version control
  • cd into this directory, cd nuxt-example2
  • run npm install simple-code-editor
  • with VS Code
    • hamburger menu on top left corner: File / Open Folder
      use folder nuxt-example2
    • open nuxt.config.js and add the line
      “simple-code-editor/nuxt”,
      inside the square brackets of modules: section
    • save nuxt.config.js
    • open pages/index.vue and edit the template section:
      • replace <Tutorial/> by
        <client-only>
        <CodeEditor value=”console.log(13)” width=”95%”
        :language_selector=”true” :languages=”[[‘cpp’,’C++’], [‘c’, ‘C’]]”></CodeEditor>
        </client-only>
      • save this file
      • use right click to delete Components/Tutorial.vue
  • go back to Termux console and run yarn dev
  • watch result with web browser, http://localhost:3000
  • to compile the final version run
    • yarn build
    • yarn generate
  • to show the content of folder dist/ at http://localhost:3000 run
    • yarn start
  • to Download the contents of the dist folder use VS Code
    • click on refresh in explorer “…”
    • right click on dist folder and select Download (only available with desktop browser; see ssh port forwarding below)
    • choose the target folder and confirm
  • to copy the dist folder to Android Documents folder (reference)
    • run termux-setup-storage
    • cp -R dist /sdcard/Documents/
    • mv /sdcard/Documents/dist/ /sdcard/Documents/code-editor2
  • this folder may be delivered by almost any simple webserver, e.g. Simple HTTP Server app on Android

Sometimes it is more comfortable to use a desktop web browser, which is possible by port forwarding with ssh.

  • open Termux console and install ssh by running pkg install openssh
  • edit /data/data/com.termux/files/usr/etc/ssh/sshd_config, e.g. with editor nano
    • add the line
      AllowTcpForwarding yes
  • use command whoami to display the linux user name used by android, e.g. u0_a250
  • set a password for this user with command passwd
  • make sure the android tablet is connected to local wifi and display the ip address with ifconfig
  • start sshd with command sshd (sshd of Termux listens on port 8022)
  • on the desktop pc run
    • ssh -p8022 -L3000:localhost:3000 -L8080:localhost:8080 u0_a123@192.168.123.123
      (replace u0_a123 with correct username and 192.168.123.123 with correct ip address)
    • open http://localhost:8080 and http://localhost:3000 with web browser

As a customer without cloud products volume licensing was done with a “normal” account of my organization: user@mydomain.xyz. Meanwhile a work or school account (WSA) is required. Reference.

Steps required, in my case, to change the existing account into a WSA account:

  • Use what is called by microsoft Internal admin takeover (reference)
  • (maybe the steps below depend on my first try to create a new account user@mydomainxyz.onmicrosoft.com account)
  • open https://powerbi.com/ and create an account with the same email address as the existing account
  • use this newly created account to open https://admin.microsoft.com/admintakeover (if asked during login wether private or business account use business account)
  • the next steps cause that the newly created account, user@mydomain.xyz, is assigned to mydomain.xyz and to the role global admin
  • you are instructed to create a dns txt entry at mydomain.xyz to verify your ownership of mydomain.xyz. (This txt dns entry can be removed after verification)
  • logged in at admin.microsoft.ch at settings/domains mydomain.xyz should be displayed as standard domain
  • now open microsoft volume licensing center, https://www.microsoft.com/Licensing/servicecenter/default.aspx and choose sign in with your work account, user@mydomain.xyz. Single sign on should work (if already logged in at admin.microsoft.com).
  • Existing licenses should be displayed. Message to upgrade account to WSA should be gone. Done!

windows 10 client: use symbolic links for Desktop, Documents and Downloads folders pointing to H: because the default install of https://www.linuxmuster.net/de/home/ uses local profile folders and network drive H: as home directory. The approach below creates this redirection of Desktop, Documents and Downloads semi automatically: all settings are made on the domain controller; if the user global-admin logs on to a workstation the symbolic links of all users which have already logged on to this workstation are created.

manage gpo by installing rsat on windows 10 (gpo and ad tools) with powershell:
Get-Command -Noun WindowsCapability
Get-WindowsCapability -Name RSAT* -Online
Get-WindowsCapability -Name RSAT* -Online | Select-Object -Property DisplayName, State
Get-WindowsCapability -Name RSAT.A* -Online | Add-WindowsCapability -Online
Get-WindowsCapability -Name RSAT.Group* -Online | Add-WindowsCapability -Online

use logon and logoff scripts (builtin gpo object sophomorix:default:school defines scripts at /var/lib/samba/sysvol/default-school/custom/windows/; create additional gpo object at global/management: sophomorix:global:management to define logon/logoff scripts for user global-admin)
User/Policies/Windows/Scripts

h: drive letter is mapped to home directory in linuxmuster, thus use
mklink /d Desktop H:\Desktop (same for Documents and Downloads). mklink requires either admin rights (elevated command prompt) or gpo policy for global-admin to allow creation of symbolic links.
Computer/Policies/Windows/Security/Local/User settings/create symbolic links

  • user scripts of normal user:
    • create H:\Desktop (same for Documents, Downloads)
    • robocopy content to H:
    • rename Desktop to Desktop.bak when logged off
      (until folder Desktop has been replaced by symlink Desktop)
    • give global admin access to main user folder
    • if symlink does not yet exist, copy content from H:\Desktop to Desktop during logon
  • logon script of global-admin
    • create symlink
  • startup script
    • truncate log file
@echo off
rem called by gpo object in schools/default-school (builtin)
rem User/Policies/Windows/Scripts
rem ################################################################
rem #          Run custom Logon-Script                             #
rem ################################################################
echo logon.bat %USERNAME% start >>c:\tmp\log.txt
if %USERNAME%==global-admin goto :ADMIN
C:
cd \Users\%USERNAME%
rem for %%d in (Videos) do (
for %%d in (Desktop Documents Downloads) do (
 if exist %%d (
  dir |find "%%d" |find "SYMLINKD"
  if not errorlevel 1 (
   if exist %%d.bak (
    if not exist H:\%%d mkdir H:\%%d
    robocopy %%d.bak H:\%%d /e /copy:DATSO /r:1 /w:3
    if not errorlevel 9 (
      attrib -r %%d.bak
      ren %%d. %%d.unused
      attrib -r -h -s %%d.unused\* /s /d
      rd %%d.unused /s /q
    ) else (
      attrib -r %%d.bak
      ren %%d.bak %%d.unused
      attrib -r -h -s %%d.unused\* /s /d
    )
   )
  ) else (
   if exist %%d.bak (
    robocopy %%d.bak %%d /e /copy:DATSO /r:1 /w:3
    attrib -r %%d.bak
    attrib -r -h -s %%d.bak\* /s /d
    rd %%d.bak /s /q
    if exist %%d.bak ren %%d.bak %%d.error
   )
   if exist H:\%%d robocopy H:\%%d %%d /e /copy:DATSO /r:1 /w:3
  )
 ) else (
   if exist %%d.bak (
    ren %%d.bak %%d
   ) else (
    mkdir %%d
   )
   if exist H:\%%d robocopy H:\%%d %%d /e /copy:DATSO /r:1 /w:3
 )
)
goto :ENDE
:ADMIN
:ENDE
@echo off
rem called by gpo object in schools/default-school (builtin)
rem User/Policies/Windows/Scripts
rem ################################################################
rem #          Run custom Logoff-Script                            #
rem ################################################################
echo logoff.bat %USERNAME% start >>c:\tmp\log.txt
if %USERNAME%==global-admin goto :ADMIN
C:
cd \Users\%USERNAME%
icacls \Users\%USERNAME% |find "global-admin" |find "(F)"
if errorlevel 1 (icacls \Users\%USERNAME% /grant "linuxmuster\global-admin":"(NP)(F)" /c)
rem for %%d in (Videos) do (
for %%d in (Desktop Documents Downloads) do (
 if exist %%d (
  dir |find "%%d" |find "SYMLINKD"
  if errorlevel 1 (
   if not exist H:\%%d mkdir H:\%%d
   robocopy %%d H:\%%d /e /copy:DATSO /r:1 /w:3
   if not errorlevel 9 (
    attrib -r %%d
    ren %%d %%d.bak
    attrib -r -h -s %%d.bak\* /s /d
    rd %%d.bak /s /q
   ) else (
    attrib -r %%d
    ren %%d %%d.bak
    attrib -r -h -s %%d.bak\* /s /d
   )
  )
 )
)

goto :ENDE
:ADMIN
:ENDE
echo logoff.bat %USERNAME% stop >>c:\tmp\log.txt
@echo off
rem called by gpo object in global/management (not builtin)
rem User/Policies/Windows/Scripts
rem ################################################################
rem #          Run custom Logon-Script                             #
rem ################################################################
echo logon.bat %USERNAME% start >>c:\tmp\log.txt
if %USERNAME%==global-admin goto :ADMIN
goto :ENDE
:ADMIN
rem user global-admin has symlink create permission in global gpo
rem Computer/Policies/Windows/Security/Local/User permissions: create symbolic links
C:
cd \Users
rem create symlinks in user directories pointing to H:\DIRNAME **only** if
rem  1) user global admin has access to user directory
rem  2) folders Desktop, Documents and/or Downloads do not exist
rem all other work is done by logon/logoff scripts of normal users
for /d %%u in (*) do (
 dir %%u
 if not errorlevel 1 (
  cd \Users\%%u
  for %%d in (Desktop Documents Downloads) do (
   if not exist %%d (
    mklink /d %%d H:\%%d
    if not errorlevel 1 (echo mklink ok %%u\%%d >>c:\tmp\log.txt) else (echo mklink failed %%u\%%d >>c:\tmp\log.txt)
   )
  )
  cd \Users
 )
)

:ENDE
echo logon.bat %USERNAME% stop >>c:\tmp\log.txt
@echo off
rem called by gpo object in global/management (not builtin)
rem User/Policies/Windows/Scripts
rem ################################################################
rem #          Run custom Logoff-Script                            #
rem ################################################################
echo logoff.bat %USERNAME% start >>c:\tmp\log.txt
if %USERNAME%==global-admin goto :ADMIN
goto :ENDE
:ADMIN
:ENDE
echo logoff.bat %USERNAME% stop >>c:\tmp\log.txt
@echo off
rem called by gpo object in schools/default-school (builtin)
rem Computer/Policies/Windows/Scripts (running as machine account)
rem ################################################################
rem #          Run custom Systemstart-Script                       #
rem ################################################################
rem overwrite existing logfile that is, clear log at startup
if not exist C:\tmp mkdir c:\tmp
echo sysstart %USERNAME% >C:\tmp\log.txt
:ENDE

This post collects observations when using MAix GO board with Arduino IDE, call Maixduino.

Macros how special pins on board MAix GO are called, can be found in pins_arduino.h of MAix GO., like PIN_KEY_PRESS which has an external pull up resistor (see schematic).

When testing example code of libraries function Serial.begin() seems to need a little time to initialize. Console output worked after adding 300mS delay. (How much is the minimal delay required?):

Serial.begin(115200);
delay(300);

Below libraries are listed which have example code that compiles and runs with MAix GO board (maybe the code needs minimal modification like replacing BUILTIN_LED by LED_BUILTIN; use compiler error messages – Preferences/Show verbose output during compilation):

  • WiFiEsp (included with Maixduino board addon of Arduino IDE)
  • RingBuffer by locoduino (installable with Arduino Library Manager)
    (Maybe EspRingBuffer coming as part of of WiFiEsp bundled with Maixduino, folder src/utility, is of interest as well.)
  • ArduinoJson by Benoît Blanchan (Arduino Library Manager)
  • ArduinoMqtt by Oleg Kovalenk which is based on eclipse paho (Arduino Library Manager)
    Use the example ConnectEsp8266WiFiClient and replace: ESP8266WiFi.h by WiFiEsp.h (included in Maixduino), WiFiClient by WiFiEspClient, ESP.reset() by WiFi.reset() and copy WiFi setup code from example of WiFiEsp. – If not properly initialized might stop k210 cpu with illegal instruction.
  • pubsubclient by Nick O’Leary (Arduino Library Manager). Same remarks as shown above (ArduinoMqtt).

According to Kendryte K210 datasheet (available here) the speed of the 4 uart interfaces of K210 can run with up to 5 MHz baud rate. Communication with esp8285 used as wifi chip runs by default with 115200 baud and seems to run with 2000000 baud as well. (Tested with sketch Examples/Maix Go/WiFiEsp/Test/EspDebug: use command AT+UART_CUR:2000000,8,1,0,1). AT command reference is here. Using the _CUR command means that after reboot the default settings get restored. Storing the settings permanently bears the risk to make esp8266 inaccessible (if not reflashed).

Source of AT command firmware is here at github, if you select tag v2.2.1 (examples/at). – Install compiler (folder dist in https://github.com/noduino/xtensa-toolchain or http://domoticx.com/sdk-esp8266-xtensa-architecture-toolchain/) and add it to path. Inside SDK folder copy folder examples/at to root folder of SDK. Run script gen_misc and choose: boot_v1.2+, user1.bin (or user2.bin), spi 40MHz, spi DOUT, 1024KB (512KB+512KB); result is in folder bin/. Uploading to esp8285 would require to shorten ground and the pad GPIO0 in the corner of the antenna connector (info from sipeed forum).

notes about using esptool.py to flash firmware (hide expanded file):
to verify size of flash
python utils/esptool.py --port /dev/cu.usbserial-143110 --baud 115200 flash_id

nodemcu board, 4MB=32000kbit, successfully flashed with
python utils/esptool.py --port /dev/cu.usbserial-143110 --baud 115200 write_flash 0x00000 bin/eagle.flash.bin 0x10000 bin/eagle.irom0text.bin 0x7E000 bin/blank.bin 0x3FE000 bin/blank.bin 0x3FC000 bin/esp_init_data_default.bin 
(tested by konsole of arduino ide; firmware has default baud rate of 115200; boot messages are sent with 74880 baud ->technical reference of esp8266)

esp8285, 1MB=4096kbit, should be flashable with
python utils/esptool.py --port /dev/cu.usbserial-143110 --baud 115200 write_flash 0x00000 bin/eagle.flash.bin 0x10000 bin/eagle.irom0text.bin 0x7E000 bin/blank.bin 0xFE000 bin/blank.bin 0xFC000 bin/esp_init_data_default.bin

To use WiFiEsp lib with 2000000 baud without setting it permanently WiFiEsp has to be patched: by default WiFi.init(&Serial1) function does call AT+RST which reboots esp8285. In patched version you can call WiFi.init(&Serial1, false) to avoid this reboot:

WiFiEsp_disable_at_rst_in_init.diff (hide expanded diff file):
diff -ur orig/WiFiEsp.cpp new/WiFiEsp.cpp
--- orig/WiFiEsp.cpp	2019-07-15 17:14:20.000000000 +0200
+++ new/WiFiEsp.cpp	2019-08-07 11:32:14.000000000 +0200
@@ -33,8 +33,14 @@
 
 void WiFiEspClass::init(Stream* espSerial)
 {
+	init(espSerial, true);
+}
+
+
+void WiFiEspClass::init(Stream* espSerial, bool run_at_rst)
+{
     LOGINFO(F("Initializing ESP module"));
-	EspDrv::wifiDriverInit(espSerial);
+	EspDrv::wifiDriverInit(espSerial, run_at_rst);
 }
 
 
diff -ur orig/WiFiEsp.h new/WiFiEsp.h
--- orig/WiFiEsp.h	2019-07-15 17:14:20.000000000 +0200
+++ new/WiFiEsp.h	2019-08-07 11:33:01.000000000 +0200
@@ -51,6 +51,8 @@
 	*/
 	static void init(Stream* espSerial);
 
+	static void init(Stream* espSerial, bool do_at_rst);
+
 
 	/**
 	* Get firmware version
diff -ur orig/utility/EspDrv.cpp new/utility/EspDrv.cpp
--- orig/utility/EspDrv.cpp	2019-07-15 17:14:20.000000000 +0200
+++ new/utility/EspDrv.cpp	2019-08-07 12:04:48.000000000 +0200
@@ -70,8 +70,15 @@
 uint8_t EspDrv::_remoteIp[] = {0};
 
 
+
 void EspDrv::wifiDriverInit(Stream *espSerial)
 {
+	wifiDriverInit(espSerial, true);
+}
+
+
+void EspDrv::wifiDriverInit(Stream *espSerial, bool run_at_rst)
+{
 	LOGDEBUG(F("> wifiDriverInit"));
 
 	EspDrv::espSerial = espSerial;
@@ -95,7 +102,7 @@
 		return;
 	}
 
-	reset();
+	reset(run_at_rst);
 
 	// check firmware version
 	getFwVersion();
@@ -114,13 +121,23 @@
 }
 
 
+
 void EspDrv::reset()
 {
+	reset(true);
+}
+
+
+void EspDrv::reset(bool run_at_rst)
+{
 	LOGDEBUG(F("> reset"));
 
-	sendCmd(F("AT+RST"));
-	delay(3000);
-	espEmptyBuf(false);  // empty dirty characters from the buffer
+	if(run_at_rst)
+	{
+		sendCmd(F("AT+RST"));
+		delay(3000);
+		espEmptyBuf(false);  // empty dirty characters from the buffer
+	}
 
 	// disable echo of commands
 	sendCmd(F("ATE0"));
diff -ur orig/utility/EspDrv.h new/utility/EspDrv.h
--- orig/utility/EspDrv.h	2019-07-15 17:14:20.000000000 +0200
+++ new/utility/EspDrv.h	2019-08-07 11:38:57.000000000 +0200
@@ -123,6 +123,9 @@
     static void wifiDriverInit(Stream *espSerial);
 
 
+    static void wifiDriverInit(Stream *espSerial, bool run_at_rst);
+
+
     /* Start Wifi connection with passphrase
      *
      * param ssid: Pointer to the SSID string.
@@ -279,6 +282,8 @@
 	static bool ping(const char *host);
     static void reset();
 
+    static void reset(bool do_at_rst);
+
     static void getRemoteIpAddress(IPAddress& ip);
     static uint16_t getRemotePort();
 

To be continued.

Server monitoring with graphical output required, but no need to run a monitoring suite like nagios? With existing debian lamp server you might want to use SmokePing.

Setup is fairly easy:

  • apt-get install smokeping
  • edit /etc/smokeping/config.d/General
    let cgiurl point to your server https://your_domain.tld/cgi-bin/smokeping.cgi
  • edit /etc/smokeping/config.d/Targets and define the servers which should be monitored
  • restart smokeping: systemctl restart smokeping (or smokeping –restart)
  • open https://your_domain.tld/smokeping/smokeping.cgi

Add password protection:

  • edit /etc/apache2/conf-available/smokeping.conf and add two Location sections:
    <Location /cgi-bin/smokeping.cgi>
    AuthUserFile /etc/apache2/your_password_file
    AuthName “protected”
    AuthType Digest
    AuthDigestProvider file
    require valid-user
    </Location>
    <Location /smokeping/smokeping.cgi>
    … (see above)
    </Location>
  • htdigest -c /etc/apache2/your_password_file “protected” your_user
  • apache2ctl configtest
    apache2ctl graceful

Credits go to Tobi Oetiker, the author of this well made tool!

SmokePing

some notes:

very slow disk access inside vm might be caused by balanced power plan (which is install default in server 2008r2, 2012r2 hyperv core).
use powercfg.exe -l to check powerplan and powercfg -s to set another plan, e.g. performance; check with crystalldiskmark

running some time virtual disks tend to get to small. On server 2012r2 powershell has:
get-vhd -path …, resize-vhd -path -sizebytes …GB, optimize-vhd

before moving a vhd(x) file, display acl lists using icacls (to be able to restore permissions iacl …./grant …)

… net use z:  \\1.2.3.4\sharename * /user:your_name (and net use z: /delete)

partitions inside a vhd may be resized with diskpart
– sel vdisk file=….
– attach vdisk
– sel last partition and the corresponding volume
– extend
– lis par (to check)
– detach

Excellent (!) main reference https://www.cyrius.com/debian/kirkwood/qnap/

Special case ts-221 (and probably all devices with 1GB RAM): kernel of debian stretch 4.9 (also stretch-backports 4.17) seems to fail with memory above 768M on qnap arm
(discussion: https://groups.google.com/forum/#!topic/linux.debian.ports.arm/vSaO642z8aY); dmesg shows
BUG: Bad rss-counter state mm

  • use jessie installer in expert mode and install stretch (offered by jessie installer as testing release)
  • before reboot
    • either switch to older kernel as described below
    • or use only 768MB of 1024MB ram to avoid this bug
      mount /dev and /proc (maybe /sys) inside installation directory and chroot into installation directory

      • copy /usr/share/doc/u-boot-tools/examples/qnap_ts119-219.config
        to /etc/fw_env.config (assuming package u-boot-tools has been installed)
      • then run fw_printenv bootargs which returns something like
        bootargs=console=ttyS0,115200 root=/dev/ram initrd=0xa00000,0x900000 ramdisk=34816
      • now use fw_setenv to add linux kernel boot parameter mem=768M by adding mem=768M to output of fw_printenv
        fw_setenv bootargs “console=ttyS0,115200 root=/dev/ram initrd=0xa00000,0x900000 ramdisk=34816 mem=768M”
        (takes some time to write to flash)
      • check with fw_printenv that bootargs still contains the original arguments (plus mem=768M)
      • reboot (check with free -h and cat /proc/cmdline; dmesg: Memory: … 0K highmem)
      • maybe, this has to be repeated after every linux kernel update (verify withc fw_printenv bootargs if mem=768M ist still there)

 

In case flash-kernel returns Failed to obtain MAC address, run either
ubootcfg -b 0 -f /dev/mtdblock4 -o – | grep “^ethaddr=” | sed “s/^ethaddr=//”
or fw_printenv ethaddr
which returns the mac address stored in flash; then run
iface_set_mac eth0 YOUR_QNAP_MAC_FROM_FLASH

Recovery: run this iso from qnap wiki page inside a vm or install minimal debian with isc-dhcp-server and tftpd-hpa.

To make sure that an existing harddisk gets properly reconfigured use installer option (removal of raid partitions)
guided – entire disk

  • one partion with btrfs and option noatime
  • in case of error see below

Workaround (ok for devices without fan): If status led remains blinking red-green create /etc/rc.local, make it executable
#!/bin/sh
qcontrol –direct statusled greenon
exit 0

Solution (fixes led, buzzer and fan): # qcontrol needs stretch backports: apt-get -t stretch-backports install qcontrol, so add to /etc/apt/sources.list

deb http://ftp.ch.debian.org/debian/ stretch-backports main

and /lib/systemd/system/qcontrold.services is broken: replaces lines
Requires=dev-input-by\x2dpath-platform\x2dgpio_keys\x2devent.device
After=dev-input-by\x2dpath-platform\x2dgpio_keys\x2devent.device

with
ConditionPathExists=/dev/input/by-path/platform-gpio_keys-event (kernel 4.9) or platform-gpio-keys-event (kernel 3.16)
and afterwards run
dpkg –configure qcontrol

In case one disk is used for the system and a second disk is used for data: mount the second disk in /etc/fstab with option noauto (else the system will not start if the data disk has been removed)

Install smartmontools and check disk with smartctl -a /dev/sdXN

Special case ts-221: does not run with kernel 4.9, so run debian stretch with kernel 3.6 -or use bootarg mem=768M as described above
(latest discussion: https://groups.google.com/forum/#!topic/linux.debian.ports.arm/vSaO642z8aY)

  • first install debian jessie (as described on cyrius.com for stretch but replace stretch by jessie in download links)
    • copy /boot/*, /lib/modules/*, /usr/lib/linux-image-* into a tar archive and save it on an external storage device
  • second, flash again the jesssie installer, but now run it in expert mode and install debian stretch
    • before rebooting the second last step “finish install or so” open console 2 (keys <ctrl> + <a>, then key <2>)
      • copy the saved contents of first installation (tar archive) into running installation
      • edit /usr/share/flash-kernel/functions: change force=”no” to force=”yes”
      • run update-initramfs -u -k 3.16..0-6-kirkwood (and probably you get a message to add “-t” as additional argument)
        later if kernel/initramfs get renewed, it is required to run afterwards: update-initramfs -u -k $(uname -r)

the installer does not cope well with already used disks (guided, use whole disk should work); if not, start installer in expert mode and add fdisk as installer component
(in case of running debian stretch wipefs -a removes all kind of signatures; but do double check that you will not wipe the wrong partition!)

  • open shell 2 and run cat /proc/partitions
  • use fdisk to create a new partition table (dos format) and create one big primary partion
  • create a filesystem on this partition with mkfs.ext2
  • reboot!!! And then run installer again in standard mode

Some qnap models may take more than 15 minutes until kernel and initrd are written to flash. – First reboot after installation might by succesful, even if power led keeps blinking red-green and buzzer remains silent. Led and buzzer need installation of qcontrol from backports and patched qcontrold.service file as described above.

Enable quota: btrfs quota enable /backup; btrfs quota enable /backup2
Display disk usage: btrfs sub list /backup; btrfs qgroup show /backup
unused because of to much memory usage when deleting snapshots

mkfs.btrfs shows after creation of filesystem whether incompatibilities with current kernel exist; use -O ^prop1,^prop2 if properties prop1,prop2 ar not compatible; (use wipefs to remove the newly created filesystem and recreate it using -O ^…)

Convert btrfs single partition filesystem to btrfs raid1 during debian install

  • use installer to create a btrfs root partition (noatime option) on a single partition
  • use installer to create on second disk a partition of same size without mountpoint
  • let installer write new partitions to disk
  • open second ssh connection and choose shell
    • btrfs filesystem show
    • df -h
    • btrfs device add -f /dev/PARTITION_ON_SECOND_DISK /target
    • btrfs balance start -dconvert=raid1 -mconvert=raid1 /target
    • btrfs filesystem show

Recover from failure of one drive of root filesystem on btrfs raid1

  • difficulty: if / gets remounted ro because of filesystem errors reboot into installer is required
  • prepare for rescue with installer
    • mkdir /var/backups/flash-installer
    • cd /tmp
    • wget http://ftp.debian.org/debian/dists/stretch/main/installer-armel/current/images/kirkwood/network-console/qnap/ts-21x/initrd
      wget http://ftp.debian.org/debian/dists/stretch/main/installer-armel/current/images/kirkwood/network-console/qnap/ts-21x/kernel-6282
    • dmesg |grep SoC
      ID=0x… displays wether kernel 6282 or 6281 is used
      cp initrd /var/backups/flash-installer/mtd2
      cp kernel-6282 /var/backups/flash-installer/mtd1 (or use kernel 6281)
    • to boot into installer run:
      cat /var/backups/flash-installer/mtd1 >/dev/mtdblock1
      cat /var/backups/flash-installer/mtd2 >/dev/mtdblock2
    • to restore installed system when running installer run:
      mount YOUR_PARTITION /mnt
      cat /mnt/var/backups/flash-kernel/mtd1 >/dev/mtdblock1
      cat /mnt/var/backups/flash-kernel/mtd2 >/dev/mtdblock2
  • recovery steps (if raid1 consists of /dev/sda3 and /dev/sdb3)
    • btrfs fi show
      displays whether both raid1 drives are accessible
    • echo 1 >/dev/block/sda/device/delete
      forces /dev/sda offline (e.g. in case of too many smart errors)
    • if btrfs fi show  displays that one drive is missing, e.g. sda3, do not run other btrfs commands to avoid errors which cause transition to ro filesystem, but do immediately run
      mount -o remount,degraded /dev/sdb3 /
    • attach replacement drive and check its device name with cat /proc/partitions (od dmesg), e.g sdc3
    • remove any existing btrfs data on replacement drive partition, e.g. on sdc3 by formatting it with ext4
      mkfs.ext4 -F /dev/sdc3
    • btrfs dev add -f /dev/sdc3 /
    • btrfs fi show
      • if sdc3 is shown twice, remove sdc3 with lower id from btrfs raid1
        btrfs dev del YOUR_DEVID /
      • if it is shown, that one drive is missing run
        btrfs del missing /
    • btrfs fi show
      this should know display two drives/partitions attached
    • to check for other error btrfs scrub might be run (time consuming)
    • reboot needed to remove option degraded from /proc/mounts

Switch hard disk with root filesystem

  • debian stretch on qnap sets kernel command line ROOT=”/dev/….” inside initramfs which is stored in flash. So using another hard disk requires to reflash initramfs
    • update-initramfs -u creates a new initramfs (at /boot/initrd.img-KERNEL_VERSION)
      and runs flash-kernel KERNEL_VERSION which creates /var/backups/flash-kernel/mtd2 (and mtd1) and flashes it
    • update-initramfs (/usr/share/initramfs-tools/hooks/flash_kernel_set_root) reads /etc/fstab to get the device with the root filesystem
    • use blkid YOUR_DEVICE_NAME to display the uuid of a block device
    • So to set a new root device, temporarily edit /etc/fstab (on the old root partition) to point to the new root filesystem device, run update-initramfs -u; then restore the old settings in/etc/fstab on old partition;
      verify that the new partition is correctly set in /etc/fstab on the new root partition,
    • additional step for recovery by tftp (based on instructions here)
      • backup flash partitions /dev/mtdblockN (N=0 … 5) to files mtdN
      • download installer kernel and initrd (reference)
        to determine kernel version (6281 or 6282)
        /usr/share/flash-kernel/dtb-probe/kirkwood-qnap
      • pad kernel
        dd if=kernel-628X of=kernel.pad ibs=2097152 conv=sync (with X=1 or 2)
      • create tftp img file
        cat mtd0 mtd4 mtd5 kernel.pad initrd mtd3 > F_TS-219_di
        and copy this recovery image file to another computer or storage device
    • finally reboot
    • if reboot fails, load qnap recovery live cd iso here
      • run live cd in e.g. virtualbox (use a network adapter not connected to existing lan)
      • copy recovery image file to /tftpboot/ on live cd
        • add a second network interface connected to local lan to allow upload by ssh
        • sudo su
        • ifconfig eth1 YOUR_LAN_IP
        • set a password for user ubuntu
          passwd ubuntu
        • upload img file with scp to /home/ubuntu
        • ifconfig eth1 down (because dhcp server of live cd)
        • cp img file to /tftpboot/ and replace existing img file for your qnap device
      • power off qnap and power on with reset button pressed (until double beep):
        this should reflash qnap flash partitions with debian installer

 

Example btrfs send, btrfs receive (btrfs volumes mounted at /backup and at /backup2;
transfer readonly subvolume rootf_ro from volume mounted at /backup to other volume mounted at /backup2)

  • btrfs sub snapshot -r /backup/rootfs /backup/rootfs_ro

  • btrfs send /backup/rootfs_ro |btrfs receive /backup2

Resize/shrink btrfs partition with root filesystem (e.g. openmediavault needs separate data partition);
assume that btrfs partition is mounted at /backup using /dev/sda2

  • btrfs filesystem resize 32G /backup
  • fdisk /dev/sda
    • p
    • d 2
    • n 2 default start +40G
      do not delete btrfs signature
    • p
    • w
    • q
  • partprobe
  • btrfs filesystem resize max /backup
  • btrfs filesystem show
  • cat /etc/fstab

Add openmediavault (version 4.x arrakis for debian stretch): instructions

  • echo “deb http://packages.openmediavault.org/public arrakis main” > /etc/apt/sources.list.d/openmediavault.list
  • apt-get install openmediavault-keyring
  • apt-get update
  • apt-get install openmediavault
  • omv-initsystem

Create raid0 btrfs volume and use subvolumes as shared folders:

  • mkfs.btrfs -L datafs -d raid0 /dev/sda4 /dev/sdb4
  • then use webinterface filesystems to mount this volume
    (gets mounted at /srv/dev-disk-by-label-datafs; cat /etc/fstab)
  • btrfs subvol create /srv/dev-disk-by-label-datafs/YourShare
  • then use webinterface to define shared folder YourShare
    • use shared folder / ACL if write access fails
  • (in case of macos sierra don’t) share this folder by nfs
    • currently, 2017-11-02, apply changes fails
      (tries to mount second partition included in btrfs raid0)
    • reboot from console
    • log into webinterface and apply change again: should work
    • nfs://your_ip/export/YourShare/
      (with default settings needs option insecure)
    • currently, 2017-11-02, nfs with macos sierra is awfully slow but samba sharing (activate wins server) works
      • nfs seems to have locking issues because ejecting share from macos finder fails (unless finder is forced to restart)

minidlna media player

  • apt-get install minidlna
  • btrfs subvol create /srv/dev-disk-by-label-datafs/Musik
  • omv webinterface: add shared folder Musik on device with datafs
    which creates /sharedfolders/Musik
  • edit /etc/minidlna.conf (compare with this reference)
    • inotify=no
    • friendly_name=YOUR_MEDIA_SERVER
    • media_dir=A,/sharedfolders/Musik
  • chown -R minidlna:minidlna /sharedfolders/Musik
  • create media file index from scratch
    • systemctl stop minidlna
    • rm /var/cache/minidlna/*
    • systemctl start minidlna
  • if new mediafiles have been added
    • chown -R minidlna:minidlna
    • systemctl restart minidlna
  • if minidlna does not get discovered by plugplayer (ios), then manually add server:
    http://your_ip:8200/rootDesc.xml
  • to display status of minidlna use a webbrowser:
    http://your_ip:8200

OMV-Extras:

  • download deb from here
  • upload this file from omv webinterface section plugins
  • install plugin openmediavault-omvextrasorg
  • settings / system / OMV-Extras: activate wanted repository
  • check (again) for available plugins (or apt-get update)

usage: some devices on internal network send diagnostic emails; keep those emails in internal network by setting up a smtp server which delivers emails directly to existing dovecot mail server using lmtp.

install postfix on qnap running dovecot
reference: https://wiki.qnap.com/wiki/Postfix

addgroup postfix
adduser -D -H -G postfix postfix
addgroup postdrop

export PATH=/opt/bin:/opt/sbin:$PATH
ln -s /opt/include /usr/include
cd /opt/src
wget –no-check-certificate https://de.postfix.org/ftpmirror/official/postfix-2.11.11.tar.gz
tar -xzf postfix-2.11.11.tar.gz
cd postfix-2.11.11

export CCARGS=’-I/opt/include -L/opt/lib -DDEF_COMMAND_DIR=\”/opt/sbin\” \
-I/opt/include/sasl -DUSE_SASL_AUTH -DDEF_SERVER_SASL_TYPE=\”dovecot\” \
-DHAS_SSL -I/opt/include/openssl -DUSE_TLS\
-DDEF_CONFIG_DIR=\”/opt/etc/postfix\” -DDEF_DAEMON_DIR=\”/opt/libexec/postfix\” -DDEF_DATA_DIR=\”/opt/var/lib/postfix\” \
-DDEF_MAILQ_PATH=\”/usr/bin/mailq\” -DDEF_HTML_DIR=\”/opt/share/doc/postfix/html\” -DDEF_MANPAGE_DIR=\”/opt/man\” \
-DDEF_NEWALIAS_PATH=\”/opt/bin/newaliases\” -DDEF_QUEUE_DIR=\”/opt/var/spool/postfix\” \
-DDEF_README_DIR=\”/opt/share/doc/postfix/readme\” -DDEF_SENDMAIL_PATH=\”/opt/sbin/sendmail\”‘

export AUXLIBS=’-lcrypto -lssl’

export LD_LIBRARY_PATH=/opt/lib
(else postconf, called by post-install, does not find libdb)

make tidy
make

replace #!/bin/sh by #!/opt/bin/bash in makedefs and post-install

edit post-install: search for chown
– replace chown root by chown admin
– in case of chown $owner (followed by chgrp $group) add these lines above the line with chown (at 2 places)
case $owner in root) owner=admin;; esac
case $group in root) group=administrators;; esac

make install

edit scripts:
/opt/libexec/postfix/postfix-script
replace #!/bin/sh by #!/opt/bin/bash
and add below
export PATH=/opt/bin:/opt/sbin:$PATH
export LD_LIBRARY_PATH=/opt/lib

postfix requires a domainname:

  • either workaround: so set in /etc/hosts of mail clients
    192.168.0.123 myserver myserver.local
    if hostname of the mailserver is myserver

    • set in /opt/etc/postfix/main.cf
      myhostname = myserver.local
      mydestination = myserver, myserver.local, localhost
      mynetworks = 127.0.0.0/8 192.168.123.0/24
  • or better: if you own mydomain.tld and if you have access to dns settings
    • create A record: mail.internal.mydomain.tld 192.168.0.123
    • create MX record: internal.mydomain.tld pointing to mail.internal.mydomain.tld
    • set in /opt/etc/postfix/main.cf
      myhostname = mail.internal.mydomain.tld
      mydestination = internal.mydomain.tld, myserver, myserver.local, localhost
      mynetworks = 127.0.0.0/8 192.168.123.0/24
    • use qnap web admin interface to create a normal user myname
      mailaddress is then myname@internal.mydomain.tld
      computers on 192.168.123.0/24 can send mails by smtp without authentication
      using mailserver mail.internal.mydomain.tld

for debugging: postfix logs to syslogd which does not run by default on qnap

  • start syslogd: syslogd
    display messages with: tail -f /var/log/messages
    when finished: killall syslogd
  • increase verbosity of postfix by editing /opt/libexec/postfix
    replace master -w by master -vvv -w
  • restart postfix:
    postfix stop
    postfix start

connect by lmtp with dovecot for non virtual user setup
mailbox_transport = lmtp:unix:private/dovecot-lmtp (in main.cf)
reference https://wiki2.dovecot.org/HowTo/PostfixAndDovecotSASL
and https://wiki2.dovecot.org/HowTo/PostfixDovecotLMTP

using /opt/var/spool/postfix/private/auth
and /opt/var/spool/postfix/private/dovecot-lmtp

after everything works non existing users can be rejected by postfix (main.cf)
smtpd_recipient_restrictions = reject_unverified_recipient

main config: service lmtp has to be enabled
if sasl auth is configured port 587 udp and tcp has to be added to etc services!
submission 587/tcp
submission 587/udp

so add to postfix start script
cat /etc/services |grep -q “[^0-9]587/tcp” || echo “submission 587/tcp” >>/etc/services
cat /etc/services |grep -q “[^0-9]587/udp” || echo “submission 587/udp” >>/etc/services
ln -s /opt/lib/libdb-4.2.so /lib
(last line because LD_LIBRARY_PATH=/opt/lib does not work for every subprocess of postfix)

problem when using dovecot with simple unix system users in default config:
postfix gives mail_user@mail_domain to lmtp but dovecot passdb of type shadow
and userdb of type passwd want mail_user (without @mail_domain suffix)

workaround: switch to passwd-file type of db for passdb and for userdb
which allows with args = username_format=%n to skip @mail_domain
Set in /opt/etc/dovecot/conf.d/auth-system.conf.ext:
service auth {
user = $default_internal_user
group = administrators
}
service auth-worker {
user = $default_internal_user
group = administrators
}

passdb {
driver = passwd-file
args = scheme=md5-crypt username_format=%n /etc/shadow
}
userdb {
driver = passwd-file
args = username_format=%n /etc/passwd
}

service auth {
unix_listener /opt/var/spool/postfix/private/auth {
mode = 0660
user = postfix
group = postfix
}
}

result: mails from local network can be sent to qnap users using smtp on port 25 without authentication

todo configure certs for tls; test smtp(s) auth